├── .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 |
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 |
--------------------------------------------------------------------------------