/* * Copyright 2018 Steven Gribble, Justin Hsia * * This file is the solution to an exercise problem posed during * one of the UW CSE 333 lectures (333exercises). * * 333exercises is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 333exercises is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 333exercises. If not, see . */ // Lec 24 Exercise 1 // // Write a program that creates a listening socket, accepts // connections from clients, and: // - reads a line of text from the client // - parses the line of text as a DNS name // - connects to that DNS name on port 80 // - writes a valid HTTP for "/" // - reads the reply and returns it to the client #include #include #include #include #include #include #include #include #include #include #include #include #define BUF_SIZE 1024 using std::string; // This structure stores information about a client that has // connected to us. typedef struct { int fd; struct sockaddr_storage caddr; socklen_t caddr_len; } ClientInfo; void Usage(char *progname); void PrintOut(int fd, struct sockaddr *addr, size_t addrlen); void PrintReverseDNS(struct sockaddr *addr, size_t addrlen); void HTTPRequest(ClientInfo cinfo, string hostname); int Listen(char *portnum); bool LookupName(string name, unsigned short port, struct sockaddr_storage *ret_addr, size_t *ret_addrlen); bool Connect(const struct sockaddr_storage &addr, const size_t &addrlen, int *ret_fd); // HandleClient is set up this way to later work with multithreading void *HandleClient(void *c_info); bool ReadLineFromSocket(int fd, string *retstr); int WrappedRead(int fd, unsigned char *buf, int readlen); int WrappedWrite(int fd, unsigned char *buf, int writelen); int main(int argc, char **argv) { // Expect the port number as a command line argument. if (argc != 2) { Usage(argv[0]); } // Parse the port number, or fail. unsigned short port = 0; if (sscanf(argv[1], "%hu", &port) != 1) { Usage(argv[0]); } int listen_fd = Listen(argv[1]); if (listen_fd <= 0) { // We failed to bind/listen to a socket. Quit with failure. std::cerr << "Couldn't bind to any addresses." << std::endl; return EXIT_FAILURE; } // Loop forever, accepting client connections and handling them. while (1) { // Allocate a "ClientInfo" structure to store information about // the next client connection that we receive. We'll hand this // structure off to a thread that we dispatch to handle the // client. ClientInfo *cinfo = new ClientInfo; cinfo->caddr_len = sizeof(cinfo->caddr); cinfo->fd = accept(listen_fd, reinterpret_cast(&(cinfo->caddr)), &(cinfo->caddr_len)); if (cinfo->fd < 0) { if ((errno == EAGAIN) || (errno == EINTR)) { delete cinfo; continue; } std::cerr << "Failure on accept: " << strerror(errno) << std::endl; delete cinfo; break; } // Handle this file descriptor. HandleClient(reinterpret_cast(cinfo)); } // Close up shop. close(listen_fd); return EXIT_SUCCESS; } void Usage(char *progname) { std::cerr << "usage: " << progname << " port" << std::endl; exit(EXIT_FAILURE); } void PrintOut(int fd, struct sockaddr *addr, size_t addrlen) { std::cout << "Socket [" << fd << "] is bound to:" << std::endl; if (addr->sa_family == AF_INET) { // Print out the IPV4 address and port char astring[INET_ADDRSTRLEN]; struct sockaddr_in *in4 = reinterpret_cast(addr); inet_ntop(AF_INET, &(in4->sin_addr), astring, INET_ADDRSTRLEN); std::cout << " IPv4 address " << astring; std::cout << " and port " << htons(in4->sin_port) << std::endl; } else if (addr->sa_family == AF_INET6) { // Print out the IPV4 address and port char astring[INET6_ADDRSTRLEN]; struct sockaddr_in6 *in6 = reinterpret_cast(addr); inet_ntop(AF_INET, &(in6->sin6_addr), astring, INET6_ADDRSTRLEN); std::cout << " IPv6 address " << astring; std::cout << " and port " << htons(in6->sin6_port) << std::endl; } else { std::cout << " ???? address and port ????" << std::endl; } } void PrintReverseDNS(struct sockaddr *addr, size_t addrlen) { char hostname[BUF_SIZE]; // ought to be big enough. if (getnameinfo(addr, addrlen, hostname, BUF_SIZE, nullptr, 0, 0) != 0) { sprintf(hostname, "[reverse DNS failed]"); } std::cout << " DNS name: " << hostname << std::endl; } int Listen(char *portnum) { // Populate the "hints" addrinfo structure for getaddrinfo(). // ("man addrinfo") struct addrinfo hints; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_INET6; // IPv6 (also handles IPv4 clients) hints.ai_socktype = SOCK_STREAM; // stream hints.ai_flags = AI_PASSIVE; // use wildcard "INADDR_ANY" hints.ai_flags |= AI_V4MAPPED; // use v4-mapped v6 if no v6 found hints.ai_protocol = IPPROTO_TCP; // tcp protocol hints.ai_canonname = nullptr; hints.ai_addr = nullptr; hints.ai_next = nullptr; // Use argv[1] as the string representation of our portnumber to // pass in to getaddrinfo(). getaddrinfo() returns a list of // address structures via the output parameter "result". struct addrinfo *result; int res = getaddrinfo(nullptr, portnum, &hints, &result); // Did addrinfo() fail? if (res != 0) { std::cerr << "getaddrinfo() failed: "; std::cerr << gai_strerror(res) << std::endl; return -1; } // Loop through the returned address structures until we are able // to create a socket and bind to one. The address structures are // linked in a list through the "ai_next" field of result. int listen_fd = -1; for (struct addrinfo *rp = result; rp != nullptr; rp = rp->ai_next) { listen_fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); if (listen_fd == -1) { // Creating this socket failed. So, loop to the next returned // result and try again. std::cerr << "socket() failed: " << strerror(errno) << std::endl; listen_fd = -1; continue; } // Configure the socket; we're setting a socket "option." In // particular, we set "SO_REUSEADDR", which tells the TCP stack // so make the port we bind to available again as soon as we // exit, rather than waiting for a few tens of seconds to recycle it. int optval = 1; assert(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == 0); // Try binding the socket to the address and port number returned // by getaddrinfo(). if (bind(listen_fd, rp->ai_addr, rp->ai_addrlen) == 0) { // Bind worked! Print out the information about what // we bound to. PrintOut(listen_fd, rp->ai_addr, rp->ai_addrlen); break; } // The bind failed. Close the socket, then loop back around and // try the next address/port returned by getaddrinfo(). close(listen_fd); listen_fd = -1; } // Free the structure returned by getaddrinfo(). freeaddrinfo(result); // If we failed to bind, return failure. if (listen_fd <= 0) return listen_fd; // Success. Tell the OS that we want this to be a listening socket. if (listen(listen_fd, SOMAXCONN) != 0) { std::cerr << "Failed to mark socket as listening: "; std::cerr << strerror(errno) << std::endl; close(listen_fd); return -1; } return listen_fd; } void *HandleClient(void *c_fd) { ClientInfo cinfo = *reinterpret_cast(c_fd); delete reinterpret_cast(c_fd); // Print out information about the client. std::cout << std::endl; std::cout << "New client connection" << std::endl; PrintOut(cinfo.fd, reinterpret_cast(&(cinfo.caddr)), cinfo.caddr_len); PrintReverseDNS(reinterpret_cast(&(cinfo.caddr)), cinfo.caddr_len); // Loop, reading data and doing a DNS lookup on the content. while (1) { string nextstr; bool res = ReadLineFromSocket(cinfo.fd, &nextstr); if (!res) { std::cout << " [The client disconnected.]" << std::endl; break; } std::cout << " the client sent: " << nextstr << std::endl; HTTPRequest(cinfo, nextstr); } close(cinfo.fd); return nullptr; } // Reads characters from the socket "fd" until it spots // a newline or EOF. Returns the read characters (minus the // newline) through "retstr". Returns true if something was // read, false otherwise. If returns false, customer should // close the socket. bool ReadLineFromSocket(int fd, string *retstr) { // We won't try to make this efficient; we'll keep appending // single characters to a string until we hit EOF or newline. // This means that C++ will be making a *ton* of string copies // along the way. string data; int count = 0; while (1) { unsigned char nextC; ssize_t res = read(fd, &nextC, 1); if (res == 1) { // Ignore '\r' characters if we see them. if (nextC == '\r') continue; // Stop on '\n' characters. if (nextC == '\n') { // All done! *retstr = data; return true; } // Append the character. data.append(1, nextC); count++; continue; } if (res == 0) { if (count == 0) return false; *retstr = data; return true; } if (res == -1) { if ((errno == EINTR) || (errno == EAGAIN)) continue; // Unrecoverable error. return false; } } // should never get here. assert(0); return false; } void HTTPRequest(ClientInfo cinfo, string hostname) { // Send an HTTP request for "/" to the given hostname. unsigned char buf[BUF_SIZE]; // Get an appropriate sockaddr structure. struct sockaddr_storage addr; size_t addrlen; if (!LookupName(hostname, 80, &addr, &addrlen)) { std::cerr << "LookupName failed for " << hostname << std::endl; exit(EXIT_FAILURE); } // Connect to the remote host. int socket_fd; if (!Connect(addr, addrlen, &socket_fd)) { std::cerr << "Failed to connect to " << hostname << std::endl; exit(EXIT_FAILURE); } // build HTTP GET request string request("GET / HTTP/1.1\r\nHost: "); request += hostname; request += "\r\nConnection: close\r\n\r\n"; std::cout << "sending: " << request; request.copy(reinterpret_cast(buf), request.size(), 0); WrappedWrite(socket_fd, buf, request.size()); // Loop, reading data from socket_fd and writing it to the client. while (1) { int res = WrappedRead(socket_fd, buf, BUF_SIZE); if (res <= 0) break; WrappedWrite(cinfo.fd, buf, BUF_SIZE); int wres = fwrite(buf, 1, res, stdout); if (wres != res) break; } close(socket_fd); } // Do a DNS lookup on name "name", and return through the output // parameter "ret_addr" a fully-formed struct sockaddr. The length // of that struct sockaddr is returned through "ret_addrlen". Also // initialize the port to "port". // // On failure, returns false. On success, returns true. bool LookupName(string name, unsigned short port, struct sockaddr_storage *ret_addr, size_t *ret_addrlen) { struct addrinfo hints, *results; int retval; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; // Do the lookup by invoking getaddrinfo(). if ((retval = getaddrinfo(name.c_str(), nullptr, &hints, &results)) != 0) { std::cerr << "getaddrinfo failed: "; std::cerr << gai_strerror(retval) << std::endl; return false; } // Set the port in the first result. if (results->ai_family == AF_INET) { struct sockaddr_in *v4addr = (struct sockaddr_in *) results->ai_addr; v4addr->sin_port = htons(port); } else if (results->ai_family == AF_INET6) { struct sockaddr_in6 *v6addr = (struct sockaddr_in6 *) results->ai_addr; v6addr->sin6_port = htons(port); } else { std::cerr << "getaddrinfo failed to provide an IPv4 or IPv6 address"; std::cerr << std::endl; return false; } // Return the first result. assert(results != nullptr); memcpy(ret_addr, results->ai_addr, results->ai_addrlen); *ret_addrlen = results->ai_addrlen; // Clean up. freeaddrinfo(results); return true; } // Connect to the remote host and port specified by addr/addrlen. // Return a connected socket through the output parameter "ret_fd". // // On failure, returns false. On success, returns true. bool Connect(const struct sockaddr_storage &addr, const size_t &addrlen, int *ret_fd) { // Create the socket. int socket_fd = socket(addr.ss_family, SOCK_STREAM, 0); if (socket_fd == -1) { std::cerr << "socket() failed: " << strerror(errno) << std::endl; return false; } // Connect the socket to the remote host. int res = connect(socket_fd, reinterpret_cast(&addr), addrlen); if (res == -1) { std::cerr << "connect() failed: " << strerror(errno) << std::endl; return false; } *ret_fd = socket_fd; return true; } // A wrapper around "write" that shields the caller from dealing // with the ugly issues of partial writes, EINTR, EAGAIN, and so // on. // // Write "writelen" bytes to the file descriptor fd from // the buffer "buf". Blocks the caller until either writelen // bytes have been written, or an error is encountered. Return // the total number of bytes written; if this number is less // than writelen, it's because some fatal error was encountered, // like the connection being dropped. int WrappedWrite(int fd, unsigned char *buf, int writelen) { int res, written_so_far = 0; while (written_so_far < writelen) { res = write(fd, buf + written_so_far, writelen - written_so_far); if (res == -1) { if ((errno == EAGAIN) || (errno == EINTR)) continue; break; } if (res == 0) break; written_so_far += res; } return written_so_far; } // A wrapper around "read" that shields the caller from dealing // with the ugly issues of partial reads, EINTR, EAGAIN, and so // on. // // Read at most "readlen" bytes from the file descriptor fd // into the buffer "buf". Return the number of bytes actually // read. On fatal error, return -1. If EOF is hit and no // bytes have been read, return 0. Might read fewer bytes // than requested. int WrappedRead(int fd, unsigned char *buf, int readlen) { int res; while (1) { res = read(fd, buf, readlen); if (res == -1) { if ((errno == EAGAIN) || (errno == EINTR)) continue; } break; } return res; }