Repository navigation
Expand file tree
/
Copy pathhttp_server.psh
More file actions
284 lines (241 loc) · 8.4 KB
/
Copy pathhttp_server.psh
File metadata and controls
284 lines (241 loc) · 8.4 KB
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
// A toy HTTP server.
//
// Run it, then visit http://127.0.0.1:8080/ in a browser, or:
// curl -i http://127.0.0.1:8080/time
//
// Each connection is handled by its own actor, which blocks on its own
// socket. That is the usual shape for a Plush server: the accept loop stays
// in the main actor and does nothing but hand sockets off.
let LISTEN_ADDR = "127.0.0.1:8080";
// Longest request head we will accept, to bound what one client can make
// us buffer before we give up on it
let MAX_HEAD = 16 * 1024;
// Read until the blank line that ends the request head. Returns the head as
// a string, or nil if the client went away or sent something unreasonable
fun read_head(sock) {
let head = ByteArray.with_size(MAX_HEAD);
let var len = 0;
loop {
// The head is full and still has no end in sight
if (len == MAX_HEAD)
return nil;
let num_read = $net_read(sock, head, len, MAX_HEAD - len);
// nil is the connection ending, 0 is 10 seconds passing with no
// data. Either way we give up on this client
if (num_read == nil || num_read == 0)
return nil;
len = len + num_read;
// Look for the CRLF CRLF that ends the head. Scanning from the
// start each time is fine for a head this small
for (let var i = 3; i < len; ++i) {
if (head[i - 3] == 13 && head[i - 2] == 10 &&
head[i - 1] == 13 && head[i] == 10) {
return String.from_utf8(head, 0, i + 1);
}
}
}
}
// Pull the method and path out of a request line like "GET /time HTTP/1.1"
fun parse_request_line(head) {
let lines = head.split("\r\n");
if (lines.len == 0)
return nil;
let parts = lines[0].split(" ");
if (parts.len < 2)
return nil;
return {method: parts[0], path: parts[1]};
}
// Built as bytes, ready to hand to $net_write. Content-Length is the body's
// length in UTF-8 bytes, which is what .len gives us
fun response(status, content_type, body) {
let resp = ByteArray.with_size(0);
resp.push_string("HTTP/1.1 " + status + "\r\n");
resp.push_string("Content-Type: " + content_type + "; charset=utf-8\r\n");
resp.push_string("Content-Length: " + body.len.to_s() + "\r\n");
resp.push_string("Connection: close\r\n\r\n");
resp.push_string(body);
return resp;
}
fun text(status, body) {
return response(status, "text/plain", body);
}
// A teddy bear, drawn back to front: ears, then head and body, then the
// face and its shades on top. Single-quoted strings so the markup keeps
// its own quotes
fun teddy_svg() {
return '<svg viewBox="0 0 200 250" width="200" height="250" role="img" aria-label="A teddy bear wearing aviator sunglasses">
<ellipse cx="100" cy="238" rx="62" ry="9" fill="rgba(0,0,0,0.10)"/>
<g fill="#a9744f" stroke="#6f4529" stroke-width="3">
<circle cx="62" cy="44" r="19"/>
<circle cx="138" cy="44" r="19"/>
<ellipse cx="100" cy="170" rx="52" ry="54"/>
<ellipse cx="46" cy="146" rx="17" ry="27" transform="rotate(22 46 146)"/>
<ellipse cx="154" cy="146" rx="17" ry="27" transform="rotate(-22 154 146)"/>
<ellipse cx="72" cy="214" rx="23" ry="18"/>
<ellipse cx="128" cy="214" rx="23" ry="18"/>
<circle cx="100" cy="82" r="46"/>
</g>
<g fill="#e8c9a0">
<circle cx="62" cy="44" r="9"/>
<circle cx="138" cy="44" r="9"/>
<ellipse cx="100" cy="180" rx="32" ry="36"/>
<ellipse cx="72" cy="216" rx="12" ry="9"/>
<ellipse cx="128" cy="216" rx="12" ry="9"/>
<ellipse cx="100" cy="98" rx="26" ry="20"/>
</g>
<g fill="#3a2415">
<circle cx="84" cy="74" r="6"/>
<circle cx="116" cy="74" r="6"/>
<ellipse cx="100" cy="88" rx="9" ry="6.5"/>
</g>
<g fill="#ffffff">
<circle cx="86" cy="72" r="2.1"/>
<circle cx="118" cy="72" r="2.1"/>
</g>
<defs>
<linearGradient id="lens" x1="0" y1="0" x2="0.25" y2="1">
<stop offset="0" stop-color="#44586d"/>
<stop offset="0.35" stop-color="#1a212a"/>
<stop offset="1" stop-color="#0a0d12"/>
</linearGradient>
<clipPath id="lens-l">
<path d="M68 66 C74 61.5, 85 60.5, 97.5 64.5 C97 71, 95.5 78, 92 83 C88 86.5, 80 88.6, 76 86 C70.5 83.8, 66.3 78, 68 66 Z"/>
</clipPath>
<clipPath id="lens-r">
<path d="M132 66 C126 61.5, 115 60.5, 102.5 64.5 C103 71, 104.5 78, 108 83 C112 86.5, 120 88.6, 124 86 C129.5 83.8, 133.7 78, 132 66 Z"/>
</clipPath>
</defs>
<g fill="url(#lens)" stroke="#24272c" stroke-width="2">
<path d="M68 66 C74 61.5, 85 60.5, 97.5 64.5 C97 71, 95.5 78, 92 83 C88 86.5, 80 88.6, 76 86 C70.5 83.8, 66.3 78, 68 66 Z"/>
<path d="M132 66 C126 61.5, 115 60.5, 102.5 64.5 C103 71, 104.5 78, 108 83 C112 86.5, 120 88.6, 124 86 C129.5 83.8, 133.7 78, 132 66 Z"/>
</g>
<g stroke="#ffffff" stroke-linecap="round" fill="none" opacity="0.22">
<g clip-path="url(#lens-l)">
<path d="M66 84 L84 58" stroke-width="5"/>
</g>
<g clip-path="url(#lens-r)">
<path d="M134 84 L116 58" stroke-width="5"/>
</g>
</g>
<g stroke="#2b2f36" stroke-width="2.2" stroke-linecap="round" fill="none">
<path d="M97.5 65 q2.5 3.5 5 0"/>
<path d="M68 67 L57 67"/>
<path d="M132 67 L143 67"/>
</g>
<g fill="#2b2f36">
<circle cx="68" cy="66.5" r="1.7"/>
<circle cx="132" cy="66.5" r="1.7"/>
</g>
<path d="M100 94 v6 M100 100 q -9 8 -15 1 M100 100 q 9 8 15 1"
fill="none" stroke="#3a2415" stroke-width="3" stroke-linecap="round"/>
</svg>';
}
fun home_page() {
return '<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Plush HTTP Server</title>
<style>
:root { color-scheme: light dark; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font: 16px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
background: #f4efe8;
color: #2c2118;
}
.card {
background: #fffdfa;
border: 1px solid #e4d8c8;
border-radius: 16px;
padding: 32px 40px;
text-align: center;
box-shadow: 0 8px 28px rgba(80, 54, 30, 0.12);
}
h1 { margin: 0 0 4px; font-size: 26px; letter-spacing: -0.01em; }
p.sub { margin: 0 0 20px; color: #7a6a58; }
nav a {
display: inline-block;
margin: 0 6px;
padding: 7px 15px;
border-radius: 999px;
background: #a9744f;
color: #fffdfa;
text-decoration: none;
font-size: 14px;
}
nav a:hover { background: #8d5c3c; }
@media (prefers-color-scheme: dark) {
body { background: #1d1713; color: #ece2d6; }
.card { background: #272019; border-color: #3b3128; box-shadow: none; }
p.sub { color: #a4917c; }
}
</style>
</head>
<body>
<main class="card">
<h1>Hello from Plush!</h1>
<p class="sub">Served by an actor of its own.</p>
' + teddy_svg() + '
<nav>
<a href="/time">/time</a>
<a href="/echo">/echo</a>
<a href="/nope">/nope</a>
</nav>
</main>
</body>
</html>
';
}
// Content is generated rather than read off disk, so that the example has
// no path handling for a request to abuse
fun route(req) {
if (req.path == "/") {
return response("200 OK", "text/html", home_page());
}
if (req.path == "/time") {
return text("200 OK", $time_current_ms().to_s() + "\n");
}
if (req.path == "/echo") {
return text("200 OK", req.method + " " + req.path + "\n");
}
return text("404 Not Found", "No route for " + req.path + "\n");
}
fun handle_conn(sock) {
let peer = $net_peer_addr(sock);
let head = read_head(sock);
let var resp = text("400 Bad Request", "Could not read request\n");
if (head != nil) {
let req = parse_request_line(head);
if (req == nil) {
resp = text("400 Bad Request", "Malformed request line\n");
} else {
$println(peer + " " + req.method + " " + req.path);
resp = route(req);
}
}
$net_write(sock, resp, 0, resp.len);
// We wrote the whole response, so closing both directions is right here
$net_close(sock);
}
fun main() {
let server = $net_listen(LISTEN_ADDR);
if (server == nil) {
$println("could not bind " + LISTEN_ADDR);
return;
}
$println("listening on http://" + $net_local_addr(server));
loop {
let sock = $net_accept(server);
// nil means the listening socket was closed
if (sock == nil)
break;
$actor_spawn(|| handle_conn(sock));
}
$net_close(server);
}
main();