diff --git a/build.sh b/build.sh index d817939..29949bb 100755 --- a/build.sh +++ b/build.sh @@ -11,3 +11,4 @@ $CXX $FLAGS future_test.cpp -o bin/future_test $CXX $FLAGS -O0 loop_test.cpp coroutine.cpp *.S -o bin/loop_test +$CXX $FLAGS -O0 httpd/httpd.cpp coroutine.cpp *.S -o bin/httpd diff --git a/event_loop.h b/event_loop.h index e8890cc..0feeb39 100644 --- a/event_loop.h +++ b/event_loop.h @@ -224,4 +224,18 @@ private: std::deque task_queue_; }; + +inline event_loop_t* get_event_loop() { + static event_loop_t loop; + return &loop; +} + +inline ssize_t recv(int sockfd, void* buf, size_t len, int flags) { + return get_event_loop()->sock_recv(sockfd, buf, len, flags); +} + +inline ssize_t send(int sockfd, void* buf, size_t len, int flags) { + return get_event_loop()->sock_send(sockfd, buf, len, flags); +} + } // cops diff --git a/httpd/httpd.cpp b/httpd/httpd.cpp new file mode 100644 index 0000000..25622d6 --- /dev/null +++ b/httpd/httpd.cpp @@ -0,0 +1,508 @@ +/* J. David's webserver */ +/* This is a simple webserver. + * Created November 1999 by J. David Blackstone. + * CSE 4344 (Network concepts), Prof. Zeigler + * University of Texas at Arlington + */ +/* This program compiles for Sparc Solaris 2.6. + * To compile for Linux: + * 1) Comment out the #include line. + * 2) Comment out the line that defines the variable newthread. + * 3) Comment out the two lines that run pthread_create(). + * 4) Uncomment the line that runs accept_request(). + * 5) Remove -lsocket from the Makefile. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#define ISspace(x) isspace((int)(x)) + +#define SERVER_STRING "Server: jdbhttpd/0.1.0\r\n" +#define STDIN 0 +#define STDOUT 1 +#define STDERR 2 + +void accept_request(void *); +void bad_request(int); +void cat(int, FILE *); +void cannot_execute(int); +void error_die(const char *); +void execute_cgi(int, const char *, const char *, const char *); +int get_line(int, char *, int); +void headers(int, const char *); +void not_found(int); +void serve_file(int, const char *); +int startup(u_short *); +void unimplemented(int); + +/**********************************************************************/ +/* A request has caused a call to accept() on the server port to + * return. Process the request appropriately. + * Parameters: the socket connected to the client */ +/**********************************************************************/ +void accept_request(void *arg) +{ + int client = (intptr_t)arg; + char buf[1024]; + size_t numchars; + char method[255]; + char url[255]; + char path[512]; + size_t i, j; + struct stat st; + int cgi = 0; /* becomes true if server decides this is a CGI + * program */ + char *query_string = NULL; + + numchars = get_line(client, buf, sizeof(buf)); + i = 0; j = 0; + while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) + { + method[i] = buf[i]; + i++; + } + j=i; + method[i] = '\0'; + + if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) + { + unimplemented(client); + return; + } + + if (strcasecmp(method, "POST") == 0) + cgi = 1; + + i = 0; + while (ISspace(buf[j]) && (j < numchars)) + j++; + while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) + { + url[i] = buf[j]; + i++; j++; + } + url[i] = '\0'; + + if (strcasecmp(method, "GET") == 0) + { + query_string = url; + while ((*query_string != '?') && (*query_string != '\0')) + query_string++; + if (*query_string == '?') + { + cgi = 1; + *query_string = '\0'; + query_string++; + } + } + + sprintf(path, "htdocs%s", url); + if (path[strlen(path) - 1] == '/') + strcat(path, "index.html"); + if (stat(path, &st) == -1) { + while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ + numchars = get_line(client, buf, sizeof(buf)); + not_found(client); + } + else + { + if ((st.st_mode & S_IFMT) == S_IFDIR) + strcat(path, "/index.html"); + if ((st.st_mode & S_IXUSR) || + (st.st_mode & S_IXGRP) || + (st.st_mode & S_IXOTH) ) + cgi = 1; + if (!cgi) + serve_file(client, path); + else + execute_cgi(client, path, method, query_string); + } + + close(client); +} + +/**********************************************************************/ +/* Inform the client that a request it has made has a problem. + * Parameters: client socket */ +/**********************************************************************/ +void bad_request(int client) +{ + char buf[1024]; + + sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n"); + cops::send(client, buf, sizeof(buf), 0); + sprintf(buf, "Content-type: text/html\r\n"); + cops::send(client, buf, sizeof(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, sizeof(buf), 0); + sprintf(buf, "

Your browser sent a bad request, "); + cops::send(client, buf, sizeof(buf), 0); + sprintf(buf, "such as a POST without a Content-Length.\r\n"); + cops::send(client, buf, sizeof(buf), 0); +} + +/**********************************************************************/ +/* Put the entire contents of a file out on a socket. This function + * is named after the UNIX "cat" command, because it might have been + * easier just to do something like pipe, fork, and exec("cat"). + * Parameters: the client socket descriptor + * FILE pointer for the file to cat */ +/**********************************************************************/ +void cat(int client, FILE *resource) +{ + char buf[1024]; + + fgets(buf, sizeof(buf), resource); + while (!feof(resource)) + { + cops::send(client, buf, strlen(buf), 0); + fgets(buf, sizeof(buf), resource); + } +} + +/**********************************************************************/ +/* Inform the client that a CGI script could not be executed. + * Parameter: the client socket descriptor. */ +/**********************************************************************/ +void cannot_execute(int client) +{ + char buf[1024]; + + sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Content-type: text/html\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "

Error prohibited CGI execution.\r\n"); + cops::send(client, buf, strlen(buf), 0); +} + +/**********************************************************************/ +/* Print out an error message with perror() (for system errors; based + * on value of errno, which indicates system call errors) and exit the + * program indicating an error. */ +/**********************************************************************/ +void error_die(const char *sc) +{ + perror(sc); + exit(1); +} + +/**********************************************************************/ +/* Execute a CGI script. Will need to set environment variables as + * appropriate. + * Parameters: client socket descriptor + * path to the CGI script */ +/**********************************************************************/ +void execute_cgi(int client, const char *path, + const char *method, const char *query_string) +{ + char buf[1024]; + int cgi_output[2]; + int cgi_input[2]; + pid_t pid; + int status; + int i; + char c; + int numchars = 1; + int content_length = -1; + + buf[0] = 'A'; buf[1] = '\0'; + if (strcasecmp(method, "GET") == 0) + while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ + numchars = get_line(client, buf, sizeof(buf)); + else if (strcasecmp(method, "POST") == 0) /*POST*/ + { + numchars = get_line(client, buf, sizeof(buf)); + while ((numchars > 0) && strcmp("\n", buf)) + { + buf[15] = '\0'; + if (strcasecmp(buf, "Content-Length:") == 0) + content_length = atoi(&(buf[16])); + numchars = get_line(client, buf, sizeof(buf)); + } + if (content_length == -1) { + bad_request(client); + return; + } + } + else/*HEAD or other*/ + { + } + + + if (pipe(cgi_output) < 0) { + cannot_execute(client); + return; + } + if (pipe(cgi_input) < 0) { + cannot_execute(client); + return; + } + + if ( (pid = fork()) < 0 ) { + cannot_execute(client); + return; + } + sprintf(buf, "HTTP/1.0 200 OK\r\n"); + cops::send(client, buf, strlen(buf), 0); + if (pid == 0) /* child: CGI script */ + { + char meth_env[255]; + char query_env[255]; + char length_env[255]; + + dup2(cgi_output[1], STDOUT); + dup2(cgi_input[0], STDIN); + close(cgi_output[0]); + close(cgi_input[1]); + sprintf(meth_env, "REQUEST_METHOD=%s", method); + putenv(meth_env); + if (strcasecmp(method, "GET") == 0) { + sprintf(query_env, "QUERY_STRING=%s", query_string); + putenv(query_env); + } + else { /* POST */ + sprintf(length_env, "CONTENT_LENGTH=%d", content_length); + putenv(length_env); + } + execl(path, NULL); + exit(0); + } else { /* parent */ + close(cgi_output[1]); + close(cgi_input[0]); + if (strcasecmp(method, "POST") == 0) + for (i = 0; i < content_length; i++) { + cops::recv(client, &c, 1, 0); + write(cgi_input[1], &c, 1); + } + while (read(cgi_output[0], &c, 1) > 0) + cops::send(client, &c, 1, 0); + + close(cgi_output[0]); + close(cgi_input[1]); + waitpid(pid, &status, 0); + } +} + +/**********************************************************************/ +/* Get a line from a socket, whether the line ends in a newline, + * carriage return, or a CRLF combination. Terminates the string read + * with a null character. If no newline indicator is found before the + * end of the buffer, the string is terminated with a null. If any of + * the above three line terminators is read, the last character of the + * string will be a linefeed and the string will be terminated with a + * null character. + * Parameters: the socket descriptor + * the buffer to save the data in + * the size of the buffer + * Returns: the number of bytes stored (excluding null) */ +/**********************************************************************/ +int get_line(int sock, char *buf, int size) +{ + int i = 0; + char c = '\0'; + int n; + + while ((i < size - 1) && (c != '\n')) + { + n = cops::recv(sock, &c, 1, 0); + /* DEBUG printf("%02X\n", c); */ + if (n > 0) + { + if (c == '\r') + { + n = cops::recv(sock, &c, 1, MSG_PEEK); + /* DEBUG printf("%02X\n", c); */ + if ((n > 0) && (c == '\n')) + cops::recv(sock, &c, 1, 0); + else + c = '\n'; + } + buf[i] = c; + i++; + } + else + c = '\n'; + } + buf[i] = '\0'; + + return(i); +} + +/**********************************************************************/ +/* Return the informational HTTP headers about a file. */ +/* Parameters: the socket to print the headers on + * the name of the file */ +/**********************************************************************/ +void headers(int client, const char *filename) +{ + char buf[1024]; + (void)filename; /* could use filename to determine file type */ + + strcpy(buf, "HTTP/1.0 200 OK\r\n"); + cops::send(client, buf, strlen(buf), 0); + strcpy(buf, SERVER_STRING); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Content-Type: text/html\r\n"); + cops::send(client, buf, strlen(buf), 0); + strcpy(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); +} + +/**********************************************************************/ +/* Give a client a 404 not found status message. */ +/**********************************************************************/ +void not_found(int client) +{ + char buf[1024]; + + sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, SERVER_STRING); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Content-Type: text/html\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Not Found\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "

The server could not fulfill\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "your request because the resource specified\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "is unavailable or nonexistent.\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); +} + +/**********************************************************************/ +/* Cops::Send a regular file to the client. Use headers, and report + * errors to client if they occur. + * Parameters: a pointer to a file structure produced from the socket + * file descriptor + * the name of the file to serve */ +/**********************************************************************/ +void serve_file(int client, const char *filename) +{ + FILE *resource = NULL; + int numchars = 1; + char buf[1024]; + + buf[0] = 'A'; buf[1] = '\0'; + while ((numchars > 0) && strcmp("\n", buf)) /* read & discard headers */ + numchars = get_line(client, buf, sizeof(buf)); + + resource = fopen(filename, "r"); + if (resource == NULL) + not_found(client); + else + { + headers(client, filename); + cat(client, resource); + } + fclose(resource); +} + +/**********************************************************************/ +/* This function starts the process of listening for web connections + * on a specified port. If the port is 0, then dynamically allocate a + * port and modify the original port variable to reflect the actual + * port. + * Parameters: pointer to variable containing the port to connect on + * Returns: the socket */ +/**********************************************************************/ +int startup(u_short *port) +{ + int httpd = 0; + int on = 1; + struct sockaddr_in name; + + httpd = socket(PF_INET, SOCK_STREAM, 0); + if (httpd == -1) + error_die("socket"); + memset(&name, 0, sizeof(name)); + name.sin_family = AF_INET; + name.sin_port = htons(*port); + name.sin_addr.s_addr = htonl(INADDR_ANY); + if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0) + { + error_die("setsockopt failed"); + } + if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) + error_die("bind"); + if (*port == 0) /* if dynamically allocating a port */ + { + socklen_t namelen = sizeof(name); + if (getsockname(httpd, (struct sockaddr *)&name, &namelen) == -1) + error_die("getsockname"); + *port = ntohs(name.sin_port); + } + if (listen(httpd, 5) < 0) + error_die("listen"); + return(httpd); +} + +/**********************************************************************/ +/* Inform the client that the requested web method has not been + * implemented. + * Parameter: the client socket */ +/**********************************************************************/ +void unimplemented(int client) +{ + char buf[1024]; + + sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, SERVER_STRING); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Content-Type: text/html\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "Method Not Implemented\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "

HTTP request method not supported.\r\n"); + cops::send(client, buf, strlen(buf), 0); + sprintf(buf, "\r\n"); + cops::send(client, buf, strlen(buf), 0); +} + +/**********************************************************************/ + +int main(void) +{ + u_short port = 9997; + auto loop = cops::get_event_loop(); + printf("httpd running on port %d\n", port); + loop->create_task([loop, port]() { + int server = loop->create_server("127.0.0.1", port); + while (1) { + int client_sock = loop->sock_accept(server); + loop->create_task([client_sock]() { + accept_request((void *)(intptr_t)client_sock); + }); + } + close(server); + }); + loop->run_forever(); + return 0; +}