├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── Makefile ├── README.md ├── home.html ├── ok.jpg ├── openme ├── server ├── server.c ├── server.o └── socket.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Actual behavior** 24 | If applicable, add screenshots to help explain your problem. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **I need to**: 11 | 12 | **So That**: 13 | 14 | **Acceptance Criteria** 15 | 16 | - [ ] TODO 1 17 | - [ ] TODO 2 18 | - [ ] TODO 3 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Brief Title 2 | 3 | ## Acceptance Criteria fulfillment 4 | 5 | - [ ] Task 1 6 | - [ ] Task 2 7 | - [ ] Task 3 8 | 9 | Fixes # (issue) 10 | 11 | ## Video/Screenshots 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC=gcc 2 | CFLAGS=-I. 3 | DEPS= 4 | OBJ=server.o 5 | USERID=123456789 6 | 7 | %.o: %.c $(DEPS) 8 | $(CC) -c -o $@ $< $(CFLAGS) 9 | 10 | all: server 11 | server: $(OBJ) 12 | $(CC) -o $@ $^ $(CFLAGS) 13 | 14 | clean: 15 | rm -rf *.o server *.tar.gz 16 | 17 | dist: tarball 18 | tarball: clean 19 | tar -cvzf /tmp/$(USERID).tar.gz --exclude=./.vagrant . && mv /tmp/$(USERID).tar.gz . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### High level design 2 | The server listens for incoming connections on a port 8080. and processes client requests in separate threads. This allows the server to handle multiple concurrent connections. 3 | 4 | Upon receiving a client request, the server checks if it is a valid GET request and extracts the requested file name. It then decodes the URL-encoded file name and determines the file's extension to identify the appropriate MIME type for the response. 5 | 6 | Using a case-insensitive file search, the server checks if the requested file exists in the current directory. If it does, the server constructs an HTTP response with the appropriate headers, including the determined MIME type, and sends the file's contents to the client. If the file is not found, the server sends a 404 Not Found response. 7 | 8 | The server continues to accept and process incoming connections until it is manually terminated. 9 | 10 | ### Problems encountered 11 | 1. After re-compiling and restarting server after every change, I got error `bind failed: address already in use`. I was confused because I've already killed every process that uses port 8080. After some [research](https://stackoverflow.com/questions/15198834/bind-failed-address-already-in-use), I found out that "the server still owns the socket when it starts and terminates quickly". So I add `SO_REUSEADDR` to the socket config, which "tells the server that even if this port is busy, go ahead and reuse it anyway". 12 | 13 | 2. After increasing buffer to 10MiB, I got error `bus error` every time I started the server. After some research, I found out that the stack memory allocated for each thread is usually limited. So I allocate the buffers on the heap instead of the stack by using `malloc()` and `free()`. 14 | -------------------------------------------------------------------------------- /home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test Page 7 | 8 | 9 |

A random cat having his time

10 |

He's truly happy

11 | Example Image 12 | 13 | 14 | -------------------------------------------------------------------------------- /ok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffreytheCoder/Simple-HTTP-Server/a5081f158a874214562f331add754bfccbdc1667/ok.jpg -------------------------------------------------------------------------------- /openme: -------------------------------------------------------------------------------- 1 | what up dude -------------------------------------------------------------------------------- /server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffreytheCoder/Simple-HTTP-Server/a5081f158a874214562f331add754bfccbdc1667/server -------------------------------------------------------------------------------- /server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define PORT 8080 19 | #define BUFFER_SIZE 104857600 20 | 21 | const char *get_file_extension(const char *file_name) { 22 | const char *dot = strrchr(file_name, '.'); 23 | if (!dot || dot == file_name) { 24 | return ""; 25 | } 26 | return dot + 1; 27 | } 28 | 29 | const char *get_mime_type(const char *file_ext) { 30 | if (strcasecmp(file_ext, "html") == 0 || strcasecmp(file_ext, "htm") == 0) { 31 | return "text/html"; 32 | } else if (strcasecmp(file_ext, "txt") == 0) { 33 | return "text/plain"; 34 | } else if (strcasecmp(file_ext, "jpg") == 0 || strcasecmp(file_ext, "jpeg") == 0) { 35 | return "image/jpeg"; 36 | } else if (strcasecmp(file_ext, "png") == 0) { 37 | return "image/png"; 38 | } else { 39 | return "application/octet-stream"; 40 | } 41 | } 42 | 43 | bool case_insensitive_compare(const char *s1, const char *s2) { 44 | while (*s1 && *s2) { 45 | if (tolower((unsigned char)*s1) != tolower((unsigned char)*s2)) { 46 | return false; 47 | } 48 | s1++; 49 | s2++; 50 | } 51 | return *s1 == *s2; 52 | } 53 | 54 | char *get_file_case_insensitive(const char *file_name) { 55 | DIR *dir = opendir("."); 56 | if (dir == NULL) { 57 | perror("opendir"); 58 | return NULL; 59 | } 60 | 61 | struct dirent *entry; 62 | char *found_file_name = NULL; 63 | while ((entry = readdir(dir)) != NULL) { 64 | if (case_insensitive_compare(entry->d_name, file_name)) { 65 | found_file_name = entry->d_name; 66 | break; 67 | } 68 | } 69 | 70 | closedir(dir); 71 | return found_file_name; 72 | } 73 | 74 | char *url_decode(const char *src) { 75 | size_t src_len = strlen(src); 76 | char *decoded = malloc(src_len + 1); 77 | size_t decoded_len = 0; 78 | 79 | // decode %2x to hex 80 | for (size_t i = 0; i < src_len; i++) { 81 | if (src[i] == '%' && i + 2 < src_len) { 82 | int hex_val; 83 | sscanf(src + i + 1, "%2x", &hex_val); 84 | decoded[decoded_len++] = hex_val; 85 | i += 2; 86 | } else { 87 | decoded[decoded_len++] = src[i]; 88 | } 89 | } 90 | 91 | // add null terminator 92 | decoded[decoded_len] = '\0'; 93 | return decoded; 94 | } 95 | 96 | void build_http_response(const char *file_name, 97 | const char *file_ext, 98 | char *response, 99 | size_t *response_len) { 100 | // build HTTP header 101 | const char *mime_type = get_mime_type(file_ext); 102 | char *header = (char *)malloc(BUFFER_SIZE * sizeof(char)); 103 | snprintf(header, BUFFER_SIZE, 104 | "HTTP/1.1 200 OK\r\n" 105 | "Content-Type: %s\r\n" 106 | "\r\n", 107 | mime_type); 108 | 109 | // if file not exist, response is 404 Not Found 110 | int file_fd = open(file_name, O_RDONLY); 111 | if (file_fd == -1) { 112 | snprintf(response, BUFFER_SIZE, 113 | "HTTP/1.1 404 Not Found\r\n" 114 | "Content-Type: text/plain\r\n" 115 | "\r\n" 116 | "404 Not Found"); 117 | *response_len = strlen(response); 118 | return; 119 | } 120 | 121 | // get file size for Content-Length 122 | struct stat file_stat; 123 | fstat(file_fd, &file_stat); 124 | off_t file_size = file_stat.st_size; 125 | 126 | // copy header to response buffer 127 | *response_len = 0; 128 | memcpy(response, header, strlen(header)); 129 | *response_len += strlen(header); 130 | 131 | // copy file to response buffer 132 | ssize_t bytes_read; 133 | while ((bytes_read = read(file_fd, 134 | response + *response_len, 135 | BUFFER_SIZE - *response_len)) > 0) { 136 | *response_len += bytes_read; 137 | } 138 | free(header); 139 | close(file_fd); 140 | } 141 | 142 | void *handle_client(void *arg) { 143 | int client_fd = *((int *)arg); 144 | char *buffer = (char *)malloc(BUFFER_SIZE * sizeof(char)); 145 | 146 | // receive request data from client and store into buffer 147 | ssize_t bytes_received = recv(client_fd, buffer, BUFFER_SIZE, 0); 148 | if (bytes_received > 0) { 149 | // check if request is GET 150 | regex_t regex; 151 | regcomp(®ex, "^GET /([^ ]*) HTTP/1", REG_EXTENDED); 152 | regmatch_t matches[2]; 153 | 154 | if (regexec(®ex, buffer, 2, matches, 0) == 0) { 155 | // extract filename from request and decode URL 156 | buffer[matches[1].rm_eo] = '\0'; 157 | const char *url_encoded_file_name = buffer + matches[1].rm_so; 158 | char *file_name = url_decode(url_encoded_file_name); 159 | 160 | // get file extension 161 | char file_ext[32]; 162 | strcpy(file_ext, get_file_extension(file_name)); 163 | 164 | // build HTTP response 165 | char *response = (char *)malloc(BUFFER_SIZE * 2 * sizeof(char)); 166 | size_t response_len; 167 | build_http_response(file_name, file_ext, response, &response_len); 168 | 169 | // send HTTP response to client 170 | send(client_fd, response, response_len, 0); 171 | 172 | free(response); 173 | free(file_name); 174 | } 175 | regfree(®ex); 176 | } 177 | close(client_fd); 178 | free(arg); 179 | free(buffer); 180 | return NULL; 181 | } 182 | 183 | int main(int argc, char *argv[]) { 184 | int server_fd; 185 | struct sockaddr_in server_addr; 186 | 187 | // create server socket 188 | if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 189 | perror("socket failed"); 190 | exit(EXIT_FAILURE); 191 | } 192 | 193 | // config socket 194 | server_addr.sin_family = AF_INET; 195 | server_addr.sin_addr.s_addr = INADDR_ANY; 196 | server_addr.sin_port = htons(PORT); 197 | 198 | // bind socket to port 199 | if (bind(server_fd, 200 | (struct sockaddr *)&server_addr, 201 | sizeof(server_addr)) < 0) { 202 | perror("bind failed"); 203 | exit(EXIT_FAILURE); 204 | } 205 | 206 | // listen for connections 207 | if (listen(server_fd, 10) < 0) { 208 | perror("listen failed"); 209 | exit(EXIT_FAILURE); 210 | } 211 | 212 | printf("Server listening on port %d\n", PORT); 213 | while (1) { 214 | // client info 215 | struct sockaddr_in client_addr; 216 | socklen_t client_addr_len = sizeof(client_addr); 217 | int *client_fd = malloc(sizeof(int)); 218 | 219 | // accept client connection 220 | if ((*client_fd = accept(server_fd, 221 | (struct sockaddr *)&client_addr, 222 | &client_addr_len)) < 0) { 223 | perror("accept failed"); 224 | continue; 225 | } 226 | 227 | // create a new thread to handle client request 228 | pthread_t thread_id; 229 | pthread_create(&thread_id, NULL, handle_client, (void *)client_fd); 230 | pthread_detach(thread_id); 231 | } 232 | 233 | close(server_fd); 234 | return 0; 235 | } -------------------------------------------------------------------------------- /server.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeffreytheCoder/Simple-HTTP-Server/a5081f158a874214562f331add754bfccbdc1667/server.o -------------------------------------------------------------------------------- /socket.txt: -------------------------------------------------------------------------------- 1 | 2.7.1 Socket Programming with UDP 2 | In this subsection, we’ll write simple client-server programs that use UDP; in the following section, we’ll 3 | write similar programs that use TCP. 4 | Recall from Section 2.1 that processes running on different machines communicate with each other by 5 | sending messages into sockets. We said that each process is analogous to a house and the process’s 6 | socket is analogous to a door. The application resides on one side of the door in the house; the 7 | transport-layer protocol resides on the other side of the door in the outside world. The application 8 | developer has control of everything on the application-layer side of the socket; however, it has little 9 | control of the transport-layer side. 10 | Now let’s take a closer look at the interaction between two communicating processes that use UDP 11 | sockets. Before the sending process can push a packet of data out the socket door, when using UDP, it 12 | must first attach a destination address to the packet. After the packet passes through the sender’s 13 | socket, the Internet will use this destination address to route the packet through the Internet to the 14 | socket in the receiving process. When the packet arrives at the receiving socket, the receiving process 15 | will retrieve the packet through the socket, and then inspect the packet’s contents and take appropriate 16 | action. 17 | So you may be now wondering, what goes into the destination address that is attached to the packet? 18 | As you might expect, the destination host’s IP address is part of the destination address. By including 19 | the destination IP address in the packet, the routers in the Internet will be able to route the packet 20 | through the Internet to the destination host. But because a host may be running many network 21 | application processes, each with one or more sockets, it is also necessary to identify the particular 22 | socket in the destination host. When a socket is created, an identifier, called a port number, is assigned 23 | to it. So, as you might expect, the packet’s destination address also includes the socket’s port number. 24 | In summary, the sending process attaches to the packet a destination address, which consists of the 25 | destination host’s IP address and the destination socket’s port number. Moreover, as we shall soon see, 26 | the sender’s source address—consisting of the IP address of the source host and the port number of the 27 | source socket—are also attached to the packet. However, attaching the source address to the packet is 28 | typically not done by the UDP application code; instead it is automatically done by the underlying 29 | operating system. 30 | We’ll use the following simple client-server application to demonstrate socket programming for both 31 | UDP and TCP: 32 | 1. The client reads a line of characters (data) from its keyboard and sends the data to the server. 33 | 2. The server receives the data and converts the characters to uppercase. 34 | 3. The server sends the modified data to the client. 35 | 4. The client receives the modified data and displays the line on its screen. 36 | Figure 2.27 highlights the main socket-related activity of the client and server that communicate over 37 | the UDP transport service. 38 | Now let’s get our hands dirty and take a look at the client-server program pair for a UDP implementation 39 | of this simple application. We also provide a detailed, line-by-line analysis after each program. We’ll 40 | begin with the UDP client, which will send a simple application-level message to the server. In order for 41 | Figure 2.27 The client-server application using UDP 42 | the server to be able to receive and reply to the client’s message, it must be ready and running—that is, 43 | it must be running as a process before the client sends its message. 44 | The client program is called UDPClient.py, and the server program is called UDPServer.py. In order to 45 | emphasize the key issues, we intentionally provide code that is minimal. “Good code” would certainly 46 | have a few more auxiliary lines, in particular for handling error cases. For this application, we have 47 | arbitrarily chosen 12000 for the server port number. 48 | UDPClient.py 49 | Here is the code for the client side of the application: 50 | from socket import * 51 | serverName = ’hostname’ 52 | serverPort = 12000 53 | clientSocket = socket(AF_INET, SOCK_DGRAM) 54 | message = raw_input(’Input lowercase sentence:’) 55 | clientSocket.sendto(message.encode(),(serverName, serverPort)) 56 | modifiedMessage, serverAddress = clientSocket.recvfrom(2048) 57 | print(modifiedMessage.decode()) 58 | clientSocket.close() 59 | Now let’s take a look at the various lines of code in UDPClient.py. 60 | from socket import * 61 | The socket module forms the basis of all network communications in Python. By including this line, we 62 | will be able to create sockets within our program. 63 | serverName = ’hostname’ 64 | serverPort = 12000 65 | The first line sets the variable serverName to the string ‘hostname’. Here, we provide a string 66 | containing either the IP address of the server (e.g., “128.138.32.126”) or the hostname of the server 67 | (e.g., “cis.poly.edu”). If we use the hostname, then a DNS lookup will automatically be performed to get 68 | the IP address.) The second line sets the integer variable serverPort to 12000. 69 | clientSocket = socket(AF_INET, SOCK_DGRAM) 70 | This line creates the client’s socket, called clientSocket . The first parameter indicates the address 71 | family; in particular, AF_INET indicates that the underlying network is using IPv4. (Do not worry about 72 | this now—we will discuss IPv4 in Chapter 4.) The second parameter indicates that the socket is of type 73 | SOCK_DGRAM , which means it is a UDP socket (rather than a TCP socket). Note that we are not 74 | specifying the port number of the client socket when we create it; we are instead letting the operating 75 | system do this for us. Now that the client process’s door has been created, we will want to create a 76 | message to send through the door. 77 | message = raw_input(’Input lowercase sentence:’) 78 | raw_input() is a built-in function in Python. When this command is executed, the user at the client is 79 | prompted with the words “Input lowercase sentence:” The user then uses her keyboard to input a line, 80 | which is put into the variable message . Now that we have a socket and a message, we will want to 81 | send the message through the socket to the destination host. 82 | clientSocket.sendto(message.encode(),(serverName, serverPort)) 83 | In the above line, we first convert the message from string type to byte type, as we need to send bytes 84 | into a socket; this is done with the encode() method. The method sendto() attaches the destination 85 | address ( serverName, serverPort ) to the message and sends the resulting packet into the 86 | process’s socket, clientSocket . (As mentioned earlier, the source address is also attached to the 87 | packet, although this is done automatically rather than explicitly by the code.) Sending a client-to-server 88 | message via a UDP socket is that simple! After sending the packet, the client waits to receive data from 89 | the server. 90 | modifiedMessage, serverAddress = clientSocket.recvfrom(2048) 91 | With the above line, when a packet arrives from the Internet at the client’s socket, the packet’s data is 92 | put into the variable modifiedMessage and the packet’s source address is put into the variable 93 | serverAddress . The variable serverAddress contains both the server’s IP address and the 94 | server’s port number. The program UDPClient doesn’t actually need this server address information, 95 | since it already knows the server address from the outset; but this line of Python provides the server 96 | address nevertheless. The method recvfrom also takes the buffer size 2048 as input. (This buffer size 97 | works for most purposes.) 98 | print(modifiedMessage.decode()) 99 | This line prints out modifiedMessage on the user’s display, after converting the message from bytes to 100 | string. It should be the original line that the user typed, but now capitalized. 101 | clientSocket.close() 102 | This line closes the socket. The process then terminates. 103 | UDPServer.py 104 | Let’s now take a look at the server side of the application: 105 | from socket import * 106 | serverPort = 12000 107 | serverSocket = socket(AF_INET, SOCK_DGRAM) 108 | serverSocket.bind((’’, serverPort)) 109 | print(”The server is ready to receive”) 110 | while True: 111 | message, clientAddress = serverSocket.recvfrom(2048) 112 | modifiedMessage = message.decode().upper() 113 | serverSocket.sendto(modifiedMessage.encode(), clientAddress) 114 | Note that the beginning of UDPServer is similar to UDPClient. It also imports the socket module, also 115 | sets the integer variable serverPort to 12000, and also creates a socket of type SOCK_DGRAM (a 116 | UDP socket). The first line of code that is significantly different from UDPClient is: 117 | serverSocket.bind((’’, serverPort)) 118 | The above line binds (that is, assigns) the port number 12000 to the server’s socket. Thus in 119 | UDPServer, the code (written by the application developer) is explicitly assigning a port number to the 120 | socket. In this manner, when anyone sends a packet to port 12000 at the IP address of the server, that 121 | packet will be directed to this socket. UDPServer then enters a while loop; the while loop will allow 122 | UDPServer to receive and process packets from clients indefinitely. In the while loop, UDPServer waits 123 | for a packet to arrive. 124 | message, clientAddress = serverSocket.recvfrom(2048) 125 | This line of code is similar to what we saw in UDPClient. When a packet arrives at the server’s socket, 126 | the packet’s data is put into the variable message and the packet’s source address is put into the 127 | variable clientAddress . The variable clientAddress contains both the client’s IP address and the 128 | client’s port number. Here, UDPServer will make use of this address information, as it provides a return 129 | address, similar to the return address with ordinary postal mail. With this source address information, 130 | the server now knows to where it should direct its reply. 131 | modifiedMessage = message.decode().upper() 132 | This line is the heart of our simple application. It takes the line sent by the client and, after converting the 133 | message to a string, uses the method upper() to capitalize it. 134 | serverSocket.sendto(modifiedMessage.encode(), clientAddress) 135 | This last line attaches the client’s address (IP address and port number) to the capitalized message 136 | (after converting the string to bytes), and sends the resulting packet into the server’s socket. (As 137 | mentioned earlier, the server address is also attached to the packet, although this is done automatically 138 | rather than explicitly by the code.) The Internet will then deliver the packet to this client address. After 139 | the server sends the packet, it remains in the while loop, waiting for another UDP packet to arrive (from 140 | any client running on any host). 141 | To test the pair of programs, you run UDPClient.py on one host and UDPServer.py on another host. Be 142 | sure to include the proper hostname or IP address of the server in UDPClient.py. Next, you execute 143 | UDPServer.py, the compiled server program, in the server host. This creates a process in the server 144 | that idles until it is contacted by some client. Then you execute UDPClient.py, the compiled client 145 | program, in the client. This creates a process in the client. Finally, to use the application at the client, 146 | you type a sentence followed by a carriage return. 147 | To develop your own UDP client-server application, you can begin by slightly modifying the client or 148 | server programs. For example, instead of converting all the letters to uppercase, the server could count 149 | the number of times the letter s appears and return this number. Or you can modify the client so that 150 | after receiving a capitalized sentence, the user can continue to send more sentences to the server. 151 | --------------------------------------------------------------------------------