-
Notifications
You must be signed in to change notification settings - Fork 3
/
TeenyHttpd.java
176 lines (152 loc) · 6.43 KB
/
TeenyHttpd.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
package net.jonathangiles.tools.teenyhttpd;
import net.jonathangiles.tools.teenyhttpd.request.Method;
import net.jonathangiles.tools.teenyhttpd.request.QueryParams;
import net.jonathangiles.tools.teenyhttpd.request.Request;
import net.jonathangiles.tools.teenyhttpd.response.FileResponse;
import net.jonathangiles.tools.teenyhttpd.response.Response;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.LocalDateTime;
import java.util.StringTokenizer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;
/**
* The TeenyHttpd server itself - instantiating an instance of this class and calling 'start()' is all that is required
* to begin serving requests.
*/
public class TeenyHttpd {
private final int port;
private final Supplier<? extends ExecutorService> executorSupplier;
private ExecutorService executorService;
private boolean isRunning = false;
private File webroot;
/**
* Creates a single-threaded server that will work on the given port, although the server does not start until
* 'stort()' is called.
*
* @param port The port for the server to listen to.
*/
public TeenyHttpd(final int port) {
this(port, Executors::newSingleThreadExecutor);
}
/**
* Creates a server that will work on the given port, although the server does not start until 'stort()' is called.
* The executor supplier enables creating {@link ExecutorService} instances that can handle requests with a range
* of different threading models.
*
* @param port The port for the server to listen to.
* @param executorSupplier A {@link ExecutorService} instances that can handle requests with a range
* of different threading models.
*/
public TeenyHttpd(final int port, final Supplier<? extends ExecutorService> executorSupplier) {
this.port = port;
this.executorSupplier = executorSupplier;
}
/**
* Sets the root directory to look for requested files.
* @param webroot A path on the local file system for serving requested files from.
*/
public void setWebroot(final File webroot) {
this.webroot = webroot;
}
/**
* Starts the server instance.
*/
public void start() {
System.out.println("TeenyHttp server started.\nListening for connections on port : " + port + " ...\n");
isRunning = true;
executorService = executorSupplier.get();
try (final ServerSocket serverSocket = new ServerSocket(port)) {
while (isRunning) {
final Socket connect = serverSocket.accept();
executorService.execute(() -> run(connect));
}
} catch (IOException e) {
System.err.println("Server Connection error : " + e.getMessage());
}
}
/**
* Requests that the server instance stop serving requests.
*/
public void stop() {
isRunning = false;
executorService.shutdown();
}
/**
* This method is called on every request, and allows for responses to be generated as appropriate.
*
* @param request The incoming request that must be responded to.
* @return The response that will be given to the requestor.
*/
public Response serve(final Request request) {
return new FileResponse(request) {
@Override protected File getFile(final String filename) {
return new File(webroot, filename);
}
};
}
private void run(final Socket connect) {
try (final BufferedReader in = new BufferedReader(new InputStreamReader(connect.getInputStream()));
final PrintWriter out = new PrintWriter(connect.getOutputStream());
final BufferedOutputStream dataOut = new BufferedOutputStream(connect.getOutputStream())) {
// get first line of the request from the client
final String input = in.readLine();
if (input == null) {
return;
}
// we parse the request line - https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
// For now we do not care about the HTTP Version
final StringTokenizer parse = new StringTokenizer(input);
// the HTTP Method
final Method method = Method.valueOf(parse.nextToken().toUpperCase());
// we get request-uri requested. For now we assume it is an absolute path
final String requestUri = parse.nextToken().toLowerCase();
// split it at the query param, if it exists
final Request request;
if (requestUri.contains("?")) {
final String[] uriSplit = requestUri.split("\\?", 2);
// create a lazily-evaluated object to represent the query parameters
request = new Request(method, uriSplit[0], new QueryParams(uriSplit[1]));
} else {
request = new Request(method, requestUri, QueryParams.EMPTY);
}
// read (but not parse) all request headers and put them into the request.
// They will be parsed on-demand.
String line;
while (true) {
line = in.readLine();
if (line == null || line.isEmpty() || "\r\n".equals(line)) {
break;
}
request.addHeader(new Header(line));
}
final Response response = serve(request);
if (response != null) {
// write headers
out.println(response.getStatusCode().toString());
out.println("Server: TeenyHttpd from JonathanGiles.net : 1.0");
out.println("Date: " + LocalDateTime.now());
response.getHeaders().forEach(out::println);
out.println(); // empty line between header and body
out.flush(); // flush character output stream buffer
// write body
response.writeBody(dataOut);
}
} catch (IOException ioe) {
System.err.println("Server error : " + ioe);
} finally {
try {
connect.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}