CS1103 Programming Assignment Unit 7 - A web server

Questions

Please write a web server that is capable of sending files to a web browser on request. The web server creates a listening socket and accepts connections. It implements just enough of the HTTP/1.1 protocol to enable it to read requests for files and to send the requested files. It is also capable of sending error responses to the client when appropriate.

1. Instructions for use

To test this program, you need to change the value of variable rootDirectory in the code to the directory where you store the test files. I use “F:” in my code, and you can change it. Then put the test files in the directory you choose, and then enter the corresponding URL in the browser, for example, using http://localhost:50505/text.txt to ask for a text.txt file from the server. Besides, you can find the test files in the attachment, which contains a text.txt file and a directory with an index.html.

2. Programs and Test Files

2.1. WebServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

/**
* This program is web server that is capable of sending files to a web browser on request.
* The web server creates a listening socket and accept connections.
* It implements just enough of the HTTP/1.1 protocol to enable it to read requests for files and to send the requested files.
* It is also capable of sending error responses to the client when appropriate.
*/
public class WebServer {

/**
* The server listens on this port.
* Note that the port number must be greater than 1024 and lest than 65535.
*/
private final static int LISTENING_PORT = 50505;

/**
* Main program opens a server socket and listens for connection requests.
* It calls the handleConnection() method to respond to connection requests.
*
* @param args
*/
public static void main(String[] args) {
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket(LISTENING_PORT);
} catch (Exception e) {
System.out.println("Failed to create listening socket.");
return;
}
System.out.println("Listening on port " + LISTENING_PORT);
try {
while (true) {
Socket connection = serverSocket.accept();
System.out.println("\nConnection from " + connection.getRemoteSocketAddress());
// Create multiple threads to handle requests
ConnectionThread thread = new ConnectionThread(connection);
thread.start();
}
} catch (Exception e) {
System.out.println("Server socket shut down unexpectedly!");
System.out.println("Error: " + e);
System.out.println("Exiting.");
}
}

/**
* This method gets an already connected socket as a parameter. It can use that socket
* to get an InputStream and an OutputStream for communicating over the connection.
* It can read the request from the input stream and send a response to the output stream.
* Finally, it can close the connection.
*
* @param connection the connected socket that will be used to communicate with the client.
*/
public static void handleConnection(Socket connection) {
try {
String rootDirectory = "F:";

Scanner in = new Scanner(connection.getInputStream()); // Create an input stream
String line = in.nextLine().trim(); // Get the first line of the request

System.out.println("The first line of the request: " + line);

// Read the first three tokens: "GET", the file path, and "HTTP/1.1" or "HTTP/1.0"
// If you can't read three tokens, you should consider that to be an error
String[] tokens = line.trim().split("\\s+"); // Split the first line of the request
String requestMethod = tokens[0].trim();
String filePath = tokens[1].trim();
String httpVersion = tokens[2].trim();

System.out.println("requestMethod: " + requestMethod); // GET
System.out.println("filePath: " + filePath); // /path/file.txt
System.out.println("httpVersion: " + httpVersion); // HTTP/1.1

// The method in the request is not 'GET', return 501
if (!requestMethod.equals("GET")) {
sendErrorResponse(501, connection.getOutputStream());
in.close();
connection.close();
return; // exit handleConnection after sending the error response
}

// If the syntax of the request is bad, return 400
if (!httpVersion.equals("HTTP/1.0") && !httpVersion.equals("HTTP/1.1")) {
sendErrorResponse(400, connection.getOutputStream());
in.close();
connection.close();
return; // exit handleConnection after sending the error response
}

// Use the exists() of the File class to test whether the file path in the request is legal and whether the file exists
// If the path is a directory, and it has an index.html file, send the status line, headers and the index.html
// If the path is a file, send the status line, headers and the index.html
// If the path does exist, return 404
File file = new File(rootDirectory + filePath); // Create a File object to represent the file that is being requested
if (file.exists()) { // Check if the file exists
if (file.isDirectory()) { // If the file is a directory, and it has an index.html file, send this index.html
String[] content = file.list();
if (content != null) {
for (String name : content) {
if (name.equals("index.html")) {
//Send the status line, some headers, and an empty line.
String filePathOfIndex = filePath + "/index.html";
File fileIndex = new File(rootDirectory + filePathOfIndex);
sendStatusLineAndHeaders(fileIndex, connection, filePathOfIndex);
// Send the file
sendFile(fileIndex, connection.getOutputStream());
}
}
}
} else { // If the path is a file, send the file
File fileFromServer = new File(rootDirectory + filePath);
if (fileFromServer.canRead()) {
//Send the status line, some headers, and an empty line.
sendStatusLineAndHeaders(fileFromServer, connection, filePath);
// Send the file
sendFile(fileFromServer, connection.getOutputStream());
} else {
// You don't have permission to read the file, return 403
sendErrorResponse(403, connection.getOutputStream());
in.close();
connection.close();
return; // exit handleConnection after sending the error response
}
}
} else { // The file does not exit, return 404
sendErrorResponse(404, connection.getOutputStream());
in.close();
connection.close();
return; // exit handleConnection after sending the error response
}
in.close(); // Close the stream
} catch (Exception e) {
System.out.println("Error while communicating with client: " + e);
} finally {
try {
connection.close(); // make SURE connection is closed before returning!
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Connection closed.");
}
}

/**
* Send a file to the client
*
* @param file
* @param socketOut
* @throws IOException
*/
private static void sendFile(File file, OutputStream socketOut) throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = new BufferedOutputStream(socketOut);

while (true) {
int x = in.read(); // read one byte from file
if (x < 0)
break; // end of file reached
out.write(x); // write the byte to the socket
}

out.flush();
}

/**
* This program returns what kind of data is in the file
*
* @param fileName
* @return
*/
private static String getMimeType(String fileName) {
int pos = fileName.lastIndexOf('.');
if (pos < 0) // no file extension in name
return "x-application/x-unknown";
String ext = fileName.substring(pos + 1).toLowerCase();
if (ext.equals("txt")) return "text/plain";
else if (ext.equals("html")) return "text/html";
else if (ext.equals("htm")) return "text/html";
else if (ext.equals("css")) return "text/css";
else if (ext.equals("js")) return "text/javascript";
else if (ext.equals("java")) return "text/x-java";
else if (ext.equals("jpeg")) return "image/jpeg";
else if (ext.equals("jpg")) return "image/jpeg";
else if (ext.equals("png")) return "image/png";
else if (ext.equals("gif")) return "image/gif";
else if (ext.equals("ico")) return "image/x-icon";
else if (ext.equals("class")) return "application/java-vm";
else if (ext.equals("jar")) return "application/java-archive";
else if (ext.equals("zip")) return "application/zip";
else if (ext.equals("xml")) return "application/xml";
else if (ext.equals("xhtml")) return "application/xhtml+xml";
else return "x-application/x-unknown";
// Note: x-application/x-unknown is something made up;
// it will probably make the browser offer to save the file.
}

/**
* If there are no errors, send the status line with the code 200, headers, and an empty line
*
* @param file
* @param connection
* @param filePath
* @throws IOException
*/
private static void sendStatusLineAndHeaders(File file, Socket connection, String filePath) throws IOException {
PrintWriter out = new PrintWriter(connection.getOutputStream());

out.print("HTTP/1.1 200 OK" + "\r\n"); // tThe status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: " + getMimeType(filePath) + "\r\n"); // Content-Type
out.print("Content-Length: " + file.length() + "\r\n"); // Content-Length
out.print("\r\n"); // An empty line

out.flush();
}

/**
* If something goes wrong with a request,
* the server should send an error response with the headers and a page
*
* @param errorCode
* @param socketOut
*/
private static void sendErrorResponse(int errorCode, OutputStream socketOut) {
PrintWriter out = new PrintWriter(socketOut);

if (errorCode == 404) {
// Send the headers
out.print("HTTP/1.1 " + errorCode + " Not Found" + "\r\n"); // The status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: text/html" + "\r\n"); // Content-Type
out.print("\r\n"); // An empty line

// Send a page that is to be displayed in the browser to inform the user of the error
out.print("<html><head><title>Error</title></head><body>" + "\r\n");
out.print("<h2>Error: 404 Not Found</h2>" + "\r\n");
out.print("<p>The resource that you requested does not exist on this server.</p>" + "\r\n");
out.print("</body></html>" + "\r\n");
}

if (errorCode == 403) {
// Send the headers
out.print("HTTP/1.1 " + errorCode + " Forbidden" + "\r\n"); // The status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: text/html" + "\r\n"); // Content-Type
out.print("\r\n"); // An empty line

// Send a page that is to be displayed in the browser to inform the user of the error
out.print("<html><head><title>Error</title></head><body>" + "\r\n");
out.print("<h2>Error: Forbidden</h2>" + "\r\n");
out.print("<p> You don't have permission to read the file </p>" + "\r\n");
out.print("</body></html>" + "\r\n");
}

if (errorCode == 400) {
// Send the headers
out.print("HTTP/1.1 " + errorCode + " Bad Request" + "\r\n"); // The status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: text/html" + "\r\n"); // Content-Type
out.print("\r\n"); // An empty line

// Send a page that is to be displayed in the browser to inform the user of the error
out.print("<html><head><title>Error</title></head><body>" + "\r\n");
out.print("<h2>Error: Bad Request</h2>" + "\r\n");
out.print("<p> The syntax of the request is bad </p>" + "\r\n");
out.print("</body></html>" + "\r\n");
}

if (errorCode == 501) {
// Send the headers
out.print("HTTP/1.1 " + errorCode + " Not Implemented" + "\r\n"); // The status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: text/html" + "\r\n"); // Content-Type
out.print("\r\n"); // An empty line

// Send a page that is to be displayed in the browser to inform the user of the error
out.print("<html><head><title>Error</title></head><body>" + "\r\n");
out.print("<h2>Error: Not Implemented</h2>" + "\r\n");
out.print("<p> The method in the request is not \"GET\" </p>" + "\r\n");
out.print("</body></html>" + "\r\n");
}

if (errorCode == 500) {
// Send the headers
out.print("HTTP/1.1 " + errorCode + " Internal Server Error" + "\r\n"); // The status line
out.print("Connection: close" + "\r\n"); // Connection
out.print("Content-Type: text/html" + "\r\n"); // Content-Type
out.print("\r\n"); // An empty line

// Send a page that is to be displayed in the browser to inform the user of the error
out.print("<html><head><title>Error</title></head><body>" + "\r\n");
out.print("<h2>Error: Internal Server Error</h2>" + "\r\n");
out.print("<p> Some unexpected error happened in the server </p>" + "\r\n");
out.print("</body></html>" + "\r\n");
}

out.flush();
out.close();
}

/**
* This class is used to create multiple threads for requests
*/
private static class ConnectionThread extends Thread {
Socket connection;

ConnectionThread(Socket connection) {
this.connection = connection;
}

public void run() {
handleConnection(connection);
}
}
}

2.2. Test files

Please click here to download the test files.

3. Outputs

3.1. Case 1

Use the browser to send a request to the server, and ask for the text.txt file. After the server receives the request, it returns the text.txt file to the browser, and the browser displays the content of text.txt

The outputs of the browser:
The outputs of the browser_case 1

The outputs of the WebServer program:
The outputs of the WebServer program_case 1

3.2. Case 2

Use the browser to send a request to the server, but the path in the file is a directory instead of a file. After the server receives the request, it returns the index.html file in that directory to the browser, and the browser displays the content of the index.html

The outputs of the browser:
The outputs of the browser_case 2

The outputs of the WebServer program:
The outputs of the WebServer program_case 2

3.3. Case 3

Use the browser to send a request to the server, but if the path is illegal or the file does not exist, the server returns a 404 error and the corresponding HTML file to the browser, and the browser displays the content of the HTML file.

The outputs of the browser:
The outputs of the browser_case 3

The outputs of the WebServer program:
The outputs of the WebServer program_case 3

3.4. Case 4

For other error types, such as 501 error, which means that the method in the request is not “GET”, we need to use telnet to test. My specific approach is to use the command (telnet localhost 50505) to connect to the server WebServer, and then enter the command (POST /text.txt HTTP/1.1). Because the method in the request header is “POST” instead of “GET“, the server will return a 501 error and the corresponding HTML, and then the telnet side will display the content of the HTML.

The outputs of the telnet:
The outputs of the telnet_case 4

The outputs of the WebServer program:
The outputs of the WebServer program_case 4

​


This is the ending, thanks for reading.