mt-lang 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/docs/index.html +4 -3
- data/lib/milk_tea/base.rb +1 -1
- data/std/asset_pack.mt +267 -0
- data/std/async/libuv_runtime.mt +539 -0
- data/std/async/mailbox.mt +191 -0
- data/std/async/runtime.mt +87 -0
- data/std/async.mt +87 -0
- data/std/base64.mt +185 -0
- data/std/behavior_tree.mt +396 -0
- data/std/binary.mt +314 -0
- data/std/binary_heap.mt +157 -0
- data/std/bitset.mt +239 -0
- data/std/box2d.mt +535 -0
- data/std/bytes.mt +46 -0
- data/std/c/box2d.mt +1254 -0
- data/std/c/cgltf.mt +647 -0
- data/std/c/cjson.mt +106 -0
- data/std/c/crypto.mt +11 -0
- data/std/c/crypto_support.h +51 -0
- data/std/c/ctype.mt +19 -0
- data/std/c/ctype_bindgen.h +29 -0
- data/std/c/curl.mt +1223 -0
- data/std/c/enet.mt +473 -0
- data/std/c/errno.mt +41 -0
- data/std/c/errno_bindgen.h +53 -0
- data/std/c/flecs.mt +2945 -0
- data/std/c/fs.linux.mt +43 -0
- data/std/c/fs.windows.mt +43 -0
- data/std/c/fs_support.h +954 -0
- data/std/c/gl.mt +2065 -0
- data/std/c/gl_registry_helpers.h +10748 -0
- data/std/c/glfw.mt +499 -0
- data/std/c/libc.mt +167 -0
- data/std/c/libuv.mt +1491 -0
- data/std/c/math.mt +22 -0
- data/std/c/math_bindgen.h +31 -0
- data/std/c/miniaudio.mt +3420 -0
- data/std/c/pcre2.mt +735 -0
- data/std/c/process.mt +58 -0
- data/std/c/process_support.h +1147 -0
- data/std/c/raygui.mt +1602 -0
- data/std/c/raylib.mt +1245 -0
- data/std/c/raymath.mt +163 -0
- data/std/c/rlgl.mt +434 -0
- data/std/c/rpng.mt +60 -0
- data/std/c/rres.mt +187 -0
- data/std/c/sdl3.mt +4132 -0
- data/std/c/sqlite3.mt +951 -0
- data/std/c/stb_image.mt +52 -0
- data/std/c/stb_image_resize2.mt +128 -0
- data/std/c/stb_image_write.mt +19 -0
- data/std/c/stb_rect_pack.mt +38 -0
- data/std/c/stb_truetype.mt +175 -0
- data/std/c/stb_vorbis.mt +31 -0
- data/std/c/stdio.mt +90 -0
- data/std/c/steamworks.h +5769 -0
- data/std/c/steamworks.mt +4771 -0
- data/std/c/string.mt +16 -0
- data/std/c/string_bindgen.h +36 -0
- data/std/c/sync.mt +33 -0
- data/std/c/sync_support.h +219 -0
- data/std/c/terminal.mt +24 -0
- data/std/c/terminal_support.h +408 -0
- data/std/c/time.mt +26 -0
- data/std/c/tls.mt +24 -0
- data/std/c/tls_support.h +626 -0
- data/std/c/tracy.mt +53 -0
- data/std/c/zlib.mt +20 -0
- data/std/c/zlib_support.h +239 -0
- data/std/c/zstd.mt +231 -0
- data/std/cell.mt +60 -0
- data/std/cgltf.mt +115 -0
- data/std/cjson.mt +90 -0
- data/std/cli.mt +768 -0
- data/std/cookie.mt +198 -0
- data/std/counter.mt +160 -0
- data/std/crypto.mt +74 -0
- data/std/cstring.mt +13 -0
- data/std/ctype.mt +52 -0
- data/std/curl/runtime.mt +261 -0
- data/std/curl.mt +38 -0
- data/std/deque.mt +348 -0
- data/std/encoding.mt +93 -0
- data/std/enet.mt +133 -0
- data/std/env.mt +41 -0
- data/std/errno.mt +47 -0
- data/std/flecs.mt +978 -0
- data/std/fmt.mt +271 -0
- data/std/fs.linux.mt +696 -0
- data/std/fs.windows.mt +711 -0
- data/std/fsm.mt +267 -0
- data/std/gl.mt +2062 -0
- data/std/glfw.mt +471 -0
- data/std/goap.mt +342 -0
- data/std/graph.mt +521 -0
- data/std/gzip.mt +77 -0
- data/std/hash.mt +429 -0
- data/std/http/server.mt +810 -0
- data/std/http.mt +1227 -0
- data/std/intern.mt +53 -0
- data/std/jobs.mt +288 -0
- data/std/json.mt +627 -0
- data/std/libc.mt +34 -0
- data/std/libuv.mt +554 -0
- data/std/linear_algebra.mt +243 -0
- data/std/linked_map.mt +372 -0
- data/std/linked_map_view.mt +49 -0
- data/std/linked_set.mt +117 -0
- data/std/log.mt +69 -0
- data/std/map.mt +418 -0
- data/std/math.mt +24 -0
- data/std/mem/arena.mt +137 -0
- data/std/mem/endian.mt +51 -0
- data/std/mem/heap.mt +298 -0
- data/std/mem/pool.mt +164 -0
- data/std/mem/stack.mt +47 -0
- data/std/mem/tracking.mt +119 -0
- data/std/miniaudio.mt +1299 -0
- data/std/multiset.mt +191 -0
- data/std/net/channel.mt +727 -0
- data/std/net/clock.mt +237 -0
- data/std/net/discovery.mt +277 -0
- data/std/net/lobby.mt +813 -0
- data/std/net/manager.mt +539 -0
- data/std/net/mux.mt +839 -0
- data/std/net/nat.mt +122 -0
- data/std/net/packet.mt +219 -0
- data/std/net/punch.mt +162 -0
- data/std/net/rpc.mt +119 -0
- data/std/net/session.mt +1157 -0
- data/std/net/stun.mt +252 -0
- data/std/net/sync.mt +183 -0
- data/std/net/turn.mt +459 -0
- data/std/net.mt +2944 -0
- data/std/oauth2.mt +408 -0
- data/std/option.mt +51 -0
- data/std/ordered_map.mt +497 -0
- data/std/ordered_set.mt +348 -0
- data/std/path.mt +368 -0
- data/std/pcre2/runtime.mt +35 -0
- data/std/pcre2.mt +78 -0
- data/std/priority_queue.mt +55 -0
- data/std/process.mt +837 -0
- data/std/queue.mt +55 -0
- data/std/random.mt +146 -0
- data/std/raygui.mt +99 -0
- data/std/raylib/debug_console.mt +71 -0
- data/std/raylib/easing.mt +388 -0
- data/std/raylib/packed_assets.mt +255 -0
- data/std/raylib/runtime.mt +30 -0
- data/std/raylib/tracy_gpu.mt +37 -0
- data/std/raylib.mt +1510 -0
- data/std/raymath.mt +153 -0
- data/std/result.mt +89 -0
- data/std/rlgl.mt +268 -0
- data/std/rpng.mt +46 -0
- data/std/rres.mt +36 -0
- data/std/sdl3/runtime.mt +54 -0
- data/std/sdl3.mt +1731 -0
- data/std/serialize.mt +68 -0
- data/std/set.mt +124 -0
- data/std/spatial.mt +176 -0
- data/std/sqlite3.mt +151 -0
- data/std/stack.mt +55 -0
- data/std/stb_image.mt +47 -0
- data/std/stb_image_resize2.mt +41 -0
- data/std/stb_image_write.mt +17 -0
- data/std/stb_rect_pack.mt +15 -0
- data/std/stb_truetype.mt +77 -0
- data/std/stb_vorbis.mt +16 -0
- data/std/stdio.mt +88 -0
- data/std/steamworks.mt +1542 -0
- data/std/str.mt +293 -0
- data/std/string.mt +234 -0
- data/std/sync.mt +194 -0
- data/std/tar.mt +704 -0
- data/std/terminal.mt +1002 -0
- data/std/testing.mt +266 -0
- data/std/thread.mt +120 -0
- data/std/time.mt +105 -0
- data/std/tls.mt +616 -0
- data/std/toml.mt +1310 -0
- data/std/tracy.mt +42 -0
- data/std/uri.mt +118 -0
- data/std/url.mt +372 -0
- data/std/vec.mt +433 -0
- data/std/zstd.mt +94 -0
- metadata +187 -2
data/std/http/server.mt
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import std.bytes as bytes
|
|
2
|
+
import std.fmt as fmt
|
|
3
|
+
import std.fs as fs
|
|
4
|
+
import std.net as net
|
|
5
|
+
import std.path as path_ops
|
|
6
|
+
import std.stdio as stdio
|
|
7
|
+
import std.str as text
|
|
8
|
+
import std.string as string
|
|
9
|
+
import std.url as url
|
|
10
|
+
import std.vec as vec
|
|
11
|
+
|
|
12
|
+
public struct Request:
|
|
13
|
+
method: string.String
|
|
14
|
+
path: string.String
|
|
15
|
+
query: str
|
|
16
|
+
headers: vec.Vec[HttpHeader]
|
|
17
|
+
body: bytes.Bytes
|
|
18
|
+
raw_url: string.String
|
|
19
|
+
|
|
20
|
+
public struct HttpHeader:
|
|
21
|
+
name: string.String
|
|
22
|
+
value: string.String
|
|
23
|
+
|
|
24
|
+
public struct Response:
|
|
25
|
+
status_code: int
|
|
26
|
+
reason: string.String
|
|
27
|
+
headers: vec.Vec[HttpHeader]
|
|
28
|
+
body: bytes.Bytes
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
public function request_method(request: Request) -> str:
|
|
32
|
+
return request.method.as_str()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
public function request_path(request: Request) -> str:
|
|
36
|
+
return request.path.as_str()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
public function request_query(request: Request) -> str:
|
|
40
|
+
return request.query
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
public function request_query_param(request: Request, key: str) -> Option[str]:
|
|
44
|
+
if request.query.len == 0:
|
|
45
|
+
return Option[str].none
|
|
46
|
+
|
|
47
|
+
let query_str = request.query
|
|
48
|
+
var start: ptr_uint = 0
|
|
49
|
+
while start < query_str.len:
|
|
50
|
+
let amp = find_byte_from(query_str, 38, start)
|
|
51
|
+
var end = query_str.len
|
|
52
|
+
if amp < query_str.len:
|
|
53
|
+
end = amp
|
|
54
|
+
|
|
55
|
+
let pair = query_str.slice(start, end - start)
|
|
56
|
+
|
|
57
|
+
let eq = pair.find_byte(61)
|
|
58
|
+
match eq:
|
|
59
|
+
Option.none:
|
|
60
|
+
if pair.equal(key):
|
|
61
|
+
return Option[str].some(value = "")
|
|
62
|
+
Option.some as e:
|
|
63
|
+
let param_key = pair.slice(0, e.value)
|
|
64
|
+
if param_key.equal(key):
|
|
65
|
+
let param_value = pair.slice(e.value + 1, pair.len - e.value - 1)
|
|
66
|
+
|
|
67
|
+
let decoded = url.percent_decode(param_value) else:
|
|
68
|
+
return Option[str].some(value = "")
|
|
69
|
+
|
|
70
|
+
return Option[str].some(value = decoded.as_str())
|
|
71
|
+
|
|
72
|
+
if amp >= query_str.len:
|
|
73
|
+
start = query_str.len
|
|
74
|
+
else:
|
|
75
|
+
start = amp + 1
|
|
76
|
+
|
|
77
|
+
return Option[str].none
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
public function request_header(request: Request, name: str) -> Option[str]:
|
|
81
|
+
var index: ptr_uint = 0
|
|
82
|
+
while index < request.headers.len():
|
|
83
|
+
let header = request.headers.get(index) else:
|
|
84
|
+
fatal(c"http.server.request_header missing entry")
|
|
85
|
+
|
|
86
|
+
unsafe:
|
|
87
|
+
let h = read(header)
|
|
88
|
+
if ascii_case_equal(h.name.as_str(), name):
|
|
89
|
+
return Option[str].some(value = h.value.as_str())
|
|
90
|
+
|
|
91
|
+
index += 1
|
|
92
|
+
|
|
93
|
+
return Option[str].none
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
public function request_body_as_str(request: Request) -> Option[str]:
|
|
97
|
+
return request.body.as_str()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
public function create_response() -> Response:
|
|
101
|
+
return Response(
|
|
102
|
+
status_code = 200,
|
|
103
|
+
reason = string.String.from_str("OK"),
|
|
104
|
+
headers = vec.Vec[HttpHeader].create(),
|
|
105
|
+
body = bytes.Bytes.empty()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
public function response_set_status(response: ref[Response], code: int, reason: str) -> void:
|
|
110
|
+
response.status_code = code
|
|
111
|
+
response.reason.assign(reason)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
public function response_set_body(response: ref[Response], content: str) -> void:
|
|
115
|
+
let body_data = bytes.Bytes.copy(text.as_byte_span(content))
|
|
116
|
+
response.body.release()
|
|
117
|
+
response.body = body_data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
public function response_set_body_json(response: ref[Response], json_text: str) -> void:
|
|
121
|
+
response_set_body(response, json_text)
|
|
122
|
+
response_set_header(response, "Content-Type", "application/json")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
public function response_set_header(response: ref[Response], name: str, value: str) -> void:
|
|
126
|
+
var index: ptr_uint = 0
|
|
127
|
+
while index < response.headers.len():
|
|
128
|
+
let header = response.headers.get(index) else:
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
unsafe:
|
|
132
|
+
if ascii_case_equal(read(header).name.as_str(), name):
|
|
133
|
+
var existing = read(header).value
|
|
134
|
+
existing.release()
|
|
135
|
+
read(header).value = string.String.from_str(value)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
index += 1
|
|
139
|
+
|
|
140
|
+
response.headers.push(HttpHeader(
|
|
141
|
+
name = string.String.from_str(name),
|
|
142
|
+
value = string.String.from_str(value)
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
public function parse_request(raw_data: span[ubyte]) -> Result[Request, string.String]:
|
|
147
|
+
let _ = text.utf8_byte_span_as_str(raw_data) else:
|
|
148
|
+
return Result[Request, string.String].failure(error = string.String.from_str("request is not valid UTF-8"))
|
|
149
|
+
|
|
150
|
+
let end_pos = find_header_terminator(raw_data) else:
|
|
151
|
+
return Result[Request, string.String].failure(error = string.String.from_str("request headers incomplete"))
|
|
152
|
+
let inner = text.utf8_byte_span_as_str(span[ubyte](data = raw_data.data, len = end_pos)) else:
|
|
153
|
+
return Result[Request, string.String].failure(
|
|
154
|
+
error = string.String.from_str("request headers not valid UTF-8")
|
|
155
|
+
)
|
|
156
|
+
let first_line_end = find_byte(inner, 10)
|
|
157
|
+
var request_line = inner.slice(0, first_line_end)
|
|
158
|
+
if request_line.ends_with("\r"):
|
|
159
|
+
request_line = inner.slice(0, request_line.len - 1)
|
|
160
|
+
|
|
161
|
+
let method_end = find_byte(request_line, 32)
|
|
162
|
+
if method_end >= request_line.len:
|
|
163
|
+
return Result[Request, string.String].failure(error = string.String.from_str("malformed request line"))
|
|
164
|
+
|
|
165
|
+
let method = request_line.slice(0, method_end)
|
|
166
|
+
|
|
167
|
+
var path_start = method_end + 1
|
|
168
|
+
while path_start < request_line.len and request_line.byte_at(path_start) == 32:
|
|
169
|
+
path_start += 1
|
|
170
|
+
|
|
171
|
+
let path_end = find_byte_from(request_line, 32, path_start)
|
|
172
|
+
let raw_url = request_line.slice(path_start, path_end - path_start)
|
|
173
|
+
|
|
174
|
+
var path = raw_url
|
|
175
|
+
var query: str = zero[str]
|
|
176
|
+
let question = raw_url.find_byte(63)
|
|
177
|
+
if question.is_some():
|
|
178
|
+
let q = question.unwrap()
|
|
179
|
+
path = raw_url.slice(0, q)
|
|
180
|
+
query = raw_url.slice(q + 1, raw_url.len - q - 1)
|
|
181
|
+
|
|
182
|
+
var headers = vec.Vec[HttpHeader].create()
|
|
183
|
+
|
|
184
|
+
let first_crlf = find_byte_from(inner, 13, first_line_end)
|
|
185
|
+
if first_crlf < inner.len:
|
|
186
|
+
var index = first_crlf + 2
|
|
187
|
+
while index < inner.len:
|
|
188
|
+
if inner.byte_at(index) == 13:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
let line_end = find_byte_from(inner, 13, index)
|
|
192
|
+
if line_end >= inner.len:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
let line = inner.slice(index, line_end - index)
|
|
196
|
+
|
|
197
|
+
let colon = line.find_byte(58)
|
|
198
|
+
if colon.is_none():
|
|
199
|
+
break
|
|
200
|
+
let c = colon.unwrap()
|
|
201
|
+
let name_text = line.slice(0, c)
|
|
202
|
+
var value_start = c + 1
|
|
203
|
+
while value_start < line.len and line.byte_at(value_start) == 32:
|
|
204
|
+
value_start += 1
|
|
205
|
+
let value_text = line.slice(value_start, line.len - value_start)
|
|
206
|
+
|
|
207
|
+
headers.push(HttpHeader(
|
|
208
|
+
name = string.String.from_str(name_text),
|
|
209
|
+
value = string.String.from_str(value_text)
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
index = line_end + 2
|
|
213
|
+
|
|
214
|
+
var body_data = bytes.Bytes.empty()
|
|
215
|
+
let body_start = end_pos + 4
|
|
216
|
+
if body_start < raw_data.len:
|
|
217
|
+
let body_span = unsafe: span[ubyte](data = raw_data.data + body_start, len = raw_data.len - body_start)
|
|
218
|
+
body_data = bytes.Bytes.copy(body_span)
|
|
219
|
+
|
|
220
|
+
return Result[Request, string.String].success(value = Request(
|
|
221
|
+
method = string.String.from_str(method),
|
|
222
|
+
path = string.String.from_str(path),
|
|
223
|
+
query = query,
|
|
224
|
+
headers = headers,
|
|
225
|
+
body = body_data,
|
|
226
|
+
raw_url = string.String.from_str(raw_url)
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
function find_header_terminator(data: span[ubyte]) -> Option[ptr_uint]:
|
|
231
|
+
if data.len < 4:
|
|
232
|
+
return Option[ptr_uint].none
|
|
233
|
+
|
|
234
|
+
var index: ptr_uint = 0
|
|
235
|
+
while index + 3 < data.len:
|
|
236
|
+
if (
|
|
237
|
+
unsafe: read(data.data + index) == 13
|
|
238
|
+
and read(data.data + index + 1) == 10
|
|
239
|
+
and read(data.data + index + 2) == 13
|
|
240
|
+
and read(data.data + index + 3) == 10
|
|
241
|
+
):
|
|
242
|
+
return Option[ptr_uint].some(value = index)
|
|
243
|
+
index += 1
|
|
244
|
+
|
|
245
|
+
return Option[ptr_uint].none
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
function find_byte(data: str, target: ubyte) -> ptr_uint:
|
|
249
|
+
var index: ptr_uint = 0
|
|
250
|
+
while index < data.len:
|
|
251
|
+
if data.byte_at(index) == target:
|
|
252
|
+
return index
|
|
253
|
+
index += 1
|
|
254
|
+
return data.len
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
function find_byte_from(data: str, target: ubyte, start: ptr_uint) -> ptr_uint:
|
|
258
|
+
var index = start
|
|
259
|
+
while index < data.len:
|
|
260
|
+
if data.byte_at(index) == target:
|
|
261
|
+
return index
|
|
262
|
+
index += 1
|
|
263
|
+
return data.len
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
function ascii_case_equal(left: str, right: str) -> bool:
|
|
267
|
+
if left.len != right.len:
|
|
268
|
+
return false
|
|
269
|
+
|
|
270
|
+
var index: ptr_uint = 0
|
|
271
|
+
while index < left.len:
|
|
272
|
+
let a = left.byte_at(index)
|
|
273
|
+
let b = right.byte_at(index)
|
|
274
|
+
|
|
275
|
+
var lower_a = a
|
|
276
|
+
if lower_a >= 65 and lower_a <= 90:
|
|
277
|
+
lower_a += 32
|
|
278
|
+
|
|
279
|
+
var lower_b = b
|
|
280
|
+
if lower_b >= 65 and lower_b <= 90:
|
|
281
|
+
lower_b += 32
|
|
282
|
+
|
|
283
|
+
if lower_a != lower_b:
|
|
284
|
+
return false
|
|
285
|
+
index += 1
|
|
286
|
+
|
|
287
|
+
return true
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
public async function write_response(stream: net.TcpStream, response: ref[Response]) -> Result[ptr_uint, net.Error]:
|
|
291
|
+
var header_text = string.String.with_capacity(256)
|
|
292
|
+
defer header_text.release()
|
|
293
|
+
|
|
294
|
+
let reason_str = response.reason.as_str()
|
|
295
|
+
var status_text = string.String.create()
|
|
296
|
+
defer status_text.release()
|
|
297
|
+
append_ptr_uint(ref_of(status_text), ptr_uint<-response.status_code)
|
|
298
|
+
|
|
299
|
+
header_text.append("HTTP/1.1 ")
|
|
300
|
+
header_text.append(status_text.as_str())
|
|
301
|
+
header_text.push_byte(32)
|
|
302
|
+
header_text.append(reason_str)
|
|
303
|
+
header_text.append("\r\n")
|
|
304
|
+
|
|
305
|
+
var body_len_text = string.String.create()
|
|
306
|
+
defer body_len_text.release()
|
|
307
|
+
append_ptr_uint(ref_of(body_len_text), response.body.len)
|
|
308
|
+
|
|
309
|
+
var has_content_type = false
|
|
310
|
+
var index: ptr_uint = 0
|
|
311
|
+
while index < response.headers.len():
|
|
312
|
+
let header = response.headers.get(index) else:
|
|
313
|
+
fatal(c"http.server.write_response missing header")
|
|
314
|
+
|
|
315
|
+
unsafe:
|
|
316
|
+
let h = read(header)
|
|
317
|
+
header_text.append(h.name.as_str())
|
|
318
|
+
header_text.append(": ")
|
|
319
|
+
header_text.append(h.value.as_str())
|
|
320
|
+
header_text.append("\r\n")
|
|
321
|
+
|
|
322
|
+
if ascii_case_equal(h.name.as_str(), "Content-Type"):
|
|
323
|
+
has_content_type = true
|
|
324
|
+
|
|
325
|
+
index += 1
|
|
326
|
+
|
|
327
|
+
if not has_content_type:
|
|
328
|
+
header_text.append("Content-Type: text/plain\r\n")
|
|
329
|
+
|
|
330
|
+
header_text.append("Content-Length: ")
|
|
331
|
+
header_text.append(body_len_text.as_str())
|
|
332
|
+
header_text.append("\r\nConnection: close\r\n\r\n")
|
|
333
|
+
|
|
334
|
+
let header_bytes = text.as_byte_span(header_text.as_str())
|
|
335
|
+
(await stream.write_bytes(header_bytes))?
|
|
336
|
+
if response.body.len > 0:
|
|
337
|
+
(await stream.write_bytes(response.body.as_span()))?
|
|
338
|
+
return Result[ptr_uint, net.Error].success(value = header_bytes.len + response.body.len)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
function append_ptr_uint(target: ref[string.String], value: ptr_uint) -> void:
|
|
342
|
+
var digits: array[ubyte, 32]
|
|
343
|
+
var count: ptr_uint = 0
|
|
344
|
+
if value == 0:
|
|
345
|
+
target.push_byte(48)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
var remaining = value
|
|
349
|
+
while remaining != 0:
|
|
350
|
+
let digit = remaining % 10
|
|
351
|
+
digits[count] = ubyte<-(48z + digit)
|
|
352
|
+
remaining = remaining / 10
|
|
353
|
+
count += 1
|
|
354
|
+
|
|
355
|
+
while count > 0:
|
|
356
|
+
count -= 1
|
|
357
|
+
target.push_byte(digits[count])
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
extending Request:
|
|
361
|
+
public editable function release() -> void:
|
|
362
|
+
this.method.release()
|
|
363
|
+
this.path.release()
|
|
364
|
+
this.raw_url.release()
|
|
365
|
+
|
|
366
|
+
var index: ptr_uint = 0
|
|
367
|
+
while index < this.headers.len():
|
|
368
|
+
let header = this.headers.get(index) else:
|
|
369
|
+
fatal(c"http.server.Request.release missing header")
|
|
370
|
+
|
|
371
|
+
unsafe:
|
|
372
|
+
var h = read(header)
|
|
373
|
+
h.release()
|
|
374
|
+
|
|
375
|
+
index += 1
|
|
376
|
+
|
|
377
|
+
this.headers.release()
|
|
378
|
+
this.body.release()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
extending HttpHeader:
|
|
382
|
+
public editable function release() -> void:
|
|
383
|
+
this.name.release()
|
|
384
|
+
this.value.release()
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
extending Response:
|
|
388
|
+
public editable function release() -> void:
|
|
389
|
+
this.reason.release()
|
|
390
|
+
|
|
391
|
+
var index: ptr_uint = 0
|
|
392
|
+
while index < this.headers.len():
|
|
393
|
+
let header = this.headers.get(index) else:
|
|
394
|
+
fatal(c"http.server.Response.release missing header")
|
|
395
|
+
|
|
396
|
+
unsafe:
|
|
397
|
+
var h = read(header)
|
|
398
|
+
h.release()
|
|
399
|
+
|
|
400
|
+
index += 1
|
|
401
|
+
|
|
402
|
+
this.headers.release()
|
|
403
|
+
this.body.release()
|
|
404
|
+
|
|
405
|
+
const DEFAULT_PORT: int = 8080
|
|
406
|
+
const READ_BUFFER_SIZE: ptr_uint = 4096
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
function parse_port(args: span[str]) -> int:
|
|
410
|
+
var index: ptr_uint = 0
|
|
411
|
+
while index < args.len:
|
|
412
|
+
let arg = unsafe: read(args.data + index)
|
|
413
|
+
if arg.equal("--port") or arg.equal("-p"):
|
|
414
|
+
index += 1
|
|
415
|
+
if index >= args.len:
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
let port_arg = unsafe: read(args.data + index)
|
|
419
|
+
var port: int = 0
|
|
420
|
+
var digit_index: ptr_uint = 0
|
|
421
|
+
while digit_index < port_arg.len:
|
|
422
|
+
let digit = int<-(port_arg.byte_at(digit_index)) - 48
|
|
423
|
+
if digit < 0 or digit > 9:
|
|
424
|
+
return 0
|
|
425
|
+
port = port * 10 + digit
|
|
426
|
+
digit_index += 1
|
|
427
|
+
|
|
428
|
+
if port > 0 and port < 65536:
|
|
429
|
+
return port
|
|
430
|
+
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
index += 1
|
|
434
|
+
|
|
435
|
+
return DEFAULT_PORT
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
function parse_serve_dir(args: span[str]) -> str:
|
|
439
|
+
var index: ptr_uint = 0
|
|
440
|
+
while index < args.len:
|
|
441
|
+
let arg = unsafe: read(args.data + index)
|
|
442
|
+
if arg.equal("--root") or arg.equal("-r"):
|
|
443
|
+
index += 1
|
|
444
|
+
if index >= args.len:
|
|
445
|
+
break
|
|
446
|
+
|
|
447
|
+
return unsafe: read(args.data + index)
|
|
448
|
+
|
|
449
|
+
if arg.equal("--port") or arg.equal("-p"):
|
|
450
|
+
index += 1
|
|
451
|
+
if index >= args.len:
|
|
452
|
+
break
|
|
453
|
+
|
|
454
|
+
index += 1
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
if not arg.starts_with("-"):
|
|
458
|
+
return arg
|
|
459
|
+
|
|
460
|
+
index += 1
|
|
461
|
+
|
|
462
|
+
return "."
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
function guess_mime(file_path: str) -> str:
|
|
466
|
+
let e = path_ops.extension(file_path).unwrap_or("")
|
|
467
|
+
if e.len == 0:
|
|
468
|
+
return "application/octet-stream"
|
|
469
|
+
if e.equal(".html") or e.equal(".htm"):
|
|
470
|
+
return "text/html"
|
|
471
|
+
if e.equal(".css"):
|
|
472
|
+
return "text/css"
|
|
473
|
+
if e.equal(".js"):
|
|
474
|
+
return "application/javascript"
|
|
475
|
+
if e.equal(".json"):
|
|
476
|
+
return "application/json"
|
|
477
|
+
if e.equal(".md"):
|
|
478
|
+
return "text/markdown"
|
|
479
|
+
if e.equal(".png"):
|
|
480
|
+
return "image/png"
|
|
481
|
+
if e.equal(".jpg") or e.equal(".jpeg"):
|
|
482
|
+
return "image/jpeg"
|
|
483
|
+
if e.equal(".gif"):
|
|
484
|
+
return "image/gif"
|
|
485
|
+
if e.equal(".svg"):
|
|
486
|
+
return "image/svg+xml"
|
|
487
|
+
if e.equal(".ico"):
|
|
488
|
+
return "image/x-icon"
|
|
489
|
+
if e.equal(".wasm"):
|
|
490
|
+
return "application/wasm"
|
|
491
|
+
if e.equal(".txt"):
|
|
492
|
+
return "text/plain"
|
|
493
|
+
if e.equal(".xml"):
|
|
494
|
+
return "application/xml"
|
|
495
|
+
if e.equal(".pdf"):
|
|
496
|
+
return "application/pdf"
|
|
497
|
+
if e.equal(".mp3"):
|
|
498
|
+
return "audio/mpeg"
|
|
499
|
+
if e.equal(".mp4"):
|
|
500
|
+
return "video/mp4"
|
|
501
|
+
return "application/octet-stream"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
function has_header_end(data: span[ubyte]) -> bool:
|
|
505
|
+
if data.len < 4:
|
|
506
|
+
return false
|
|
507
|
+
|
|
508
|
+
var index: ptr_uint = 0
|
|
509
|
+
while index + 3 < data.len:
|
|
510
|
+
if (
|
|
511
|
+
unsafe: read(data.data + index) == 13
|
|
512
|
+
and read(data.data + index + 1) == 10
|
|
513
|
+
and read(data.data + index + 2) == 13
|
|
514
|
+
and read(data.data + index + 3) == 10
|
|
515
|
+
):
|
|
516
|
+
return true
|
|
517
|
+
index += 1
|
|
518
|
+
|
|
519
|
+
return false
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
async function send_error(stream: net.TcpStream, code: int, message: str) -> void:
|
|
523
|
+
var code_str = fmt.to_string_int(code)
|
|
524
|
+
defer code_str.release()
|
|
525
|
+
let body_text = f"<html><body><h1>#{code_str.as_str()} #{message}</h1></body></html>\r\n"
|
|
526
|
+
var body_len_str = fmt.to_string_ptr_uint(body_text.len)
|
|
527
|
+
defer body_len_str.release()
|
|
528
|
+
let header_text = f"HTTP/1.1 #{code_str.as_str()} #{message}\r\nContent-Type: text/html\r\nContent-Length: #{body_len_str.as_str()}\r\nConnection: close\r\n\r\n"
|
|
529
|
+
await stream.write_bytes(text.as_byte_span(header_text))
|
|
530
|
+
await stream.write_bytes(text.as_byte_span(body_text))
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async function send_file_response(
|
|
534
|
+
stream: net.TcpStream,
|
|
535
|
+
code: int,
|
|
536
|
+
message: str,
|
|
537
|
+
content_type: str,
|
|
538
|
+
body: span[ubyte],
|
|
539
|
+
head_only: bool
|
|
540
|
+
) -> void:
|
|
541
|
+
var code_str = fmt.to_string_int(code)
|
|
542
|
+
defer code_str.release()
|
|
543
|
+
var body_len_str = fmt.to_string_ptr_uint(body.len)
|
|
544
|
+
defer body_len_str.release()
|
|
545
|
+
let header_text = f"HTTP/1.1 #{code_str.as_str()} #{message}\r\nContent-Type: #{content_type}\r\nContent-Length: #{body_len_str.as_str()}\r\nConnection: close\r\n\r\n"
|
|
546
|
+
await stream.write_bytes(text.as_byte_span(header_text))
|
|
547
|
+
if not head_only:
|
|
548
|
+
await stream.write_bytes(body)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
function sorted_insert(
|
|
552
|
+
names: ref[vec.Vec[string.String]],
|
|
553
|
+
dir_flags: ref[vec.Vec[bool]],
|
|
554
|
+
value: string.String,
|
|
555
|
+
is_directory: bool
|
|
556
|
+
) -> void:
|
|
557
|
+
var index: ptr_uint = 0
|
|
558
|
+
while index < names.len():
|
|
559
|
+
let existing_ptr = names.get(index) else:
|
|
560
|
+
break
|
|
561
|
+
let existing_flag_ptr = dir_flags.get(index) else:
|
|
562
|
+
break
|
|
563
|
+
|
|
564
|
+
unsafe:
|
|
565
|
+
let existing_is_dir = read(existing_flag_ptr)
|
|
566
|
+
let existing_name = read(existing_ptr).as_str()
|
|
567
|
+
|
|
568
|
+
if is_directory and not existing_is_dir:
|
|
569
|
+
break
|
|
570
|
+
if not is_directory and existing_is_dir:
|
|
571
|
+
index += 1
|
|
572
|
+
continue
|
|
573
|
+
|
|
574
|
+
let cmp = value.as_str().compare(existing_name)
|
|
575
|
+
if cmp < 0:
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
index += 1
|
|
579
|
+
|
|
580
|
+
if not names.insert(index, value):
|
|
581
|
+
fatal(c"sorted_insert names insert failed")
|
|
582
|
+
if not dir_flags.insert(index, is_directory):
|
|
583
|
+
fatal(c"sorted_insert flags insert failed")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
function html_escape(text_value: str) -> string.String:
|
|
587
|
+
var result = string.String.with_capacity(text_value.len * 2)
|
|
588
|
+
var index: ptr_uint = 0
|
|
589
|
+
while index < text_value.len:
|
|
590
|
+
let ch = text_value.byte_at(index)
|
|
591
|
+
if ch == 38:
|
|
592
|
+
result.append("&")
|
|
593
|
+
else if ch == 60:
|
|
594
|
+
result.append("<")
|
|
595
|
+
else if ch == 62:
|
|
596
|
+
result.append(">")
|
|
597
|
+
else if ch == 34:
|
|
598
|
+
result.append(""")
|
|
599
|
+
else:
|
|
600
|
+
result.push_byte(ch)
|
|
601
|
+
index += 1
|
|
602
|
+
|
|
603
|
+
return result
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
function release_string_vec(values: ref[vec.Vec[string.String]]) -> void:
|
|
607
|
+
var index: ptr_uint = 0
|
|
608
|
+
while index < values.len():
|
|
609
|
+
let value_ptr = values.get(index) else:
|
|
610
|
+
break
|
|
611
|
+
unsafe:
|
|
612
|
+
var v = read(value_ptr)
|
|
613
|
+
v.release()
|
|
614
|
+
index += 1
|
|
615
|
+
|
|
616
|
+
values.release()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
async function serve_directory_index(stream: net.TcpStream, dir_path: str, url_prefix: str, head_only: bool) -> void:
|
|
620
|
+
var entries = fs.list_entries(dir_path) else:
|
|
621
|
+
await send_error(stream, 500, "Internal Server Error")
|
|
622
|
+
return
|
|
623
|
+
defer entries.release()
|
|
624
|
+
|
|
625
|
+
var names = vec.Vec[string.String].create()
|
|
626
|
+
defer release_string_vec(ref_of(names))
|
|
627
|
+
|
|
628
|
+
var is_dir = vec.Vec[bool].create()
|
|
629
|
+
defer is_dir.release()
|
|
630
|
+
|
|
631
|
+
var index: ptr_uint = 0
|
|
632
|
+
while index < entries.len():
|
|
633
|
+
let entry_ptr = entries.get(index)
|
|
634
|
+
if entry_ptr.is_some():
|
|
635
|
+
let entry_name = entry_ptr.unwrap()
|
|
636
|
+
let name_str = entry_name
|
|
637
|
+
var child_path = path_ops.join(dir_path, name_str)
|
|
638
|
+
defer child_path.release()
|
|
639
|
+
let dir_flag = fs.is_directory(child_path.as_str())
|
|
640
|
+
sorted_insert(ref_of(names), ref_of(is_dir), string.String.from_str(name_str), dir_flag)
|
|
641
|
+
index += 1
|
|
642
|
+
|
|
643
|
+
var body = string.String.with_capacity(4096)
|
|
644
|
+
defer body.release()
|
|
645
|
+
|
|
646
|
+
var escaped_path = html_escape(url_prefix)
|
|
647
|
+
defer escaped_path.release()
|
|
648
|
+
|
|
649
|
+
body.append("<!DOCTYPE html>\n<html>\n<head><meta charset=\"utf-8\"><title>Index of ")
|
|
650
|
+
body.append(escaped_path.as_str())
|
|
651
|
+
body.append("</title></head>\n<body>\n<h1>Index of ")
|
|
652
|
+
body.append(escaped_path.as_str())
|
|
653
|
+
body.append("</h1>\n<hr>\n<pre>\n")
|
|
654
|
+
|
|
655
|
+
if url_prefix.len > 0 and not url_prefix.equal("/"):
|
|
656
|
+
body.append("<a href=\"../\">../</a>\n")
|
|
657
|
+
|
|
658
|
+
index = 0
|
|
659
|
+
while index < names.len():
|
|
660
|
+
let name_ptr = names.get(index) else:
|
|
661
|
+
break
|
|
662
|
+
let dir_flag_ptr = is_dir.get(index) else:
|
|
663
|
+
break
|
|
664
|
+
|
|
665
|
+
unsafe:
|
|
666
|
+
let name_str = read(name_ptr).as_str()
|
|
667
|
+
let is_directory = read(dir_flag_ptr)
|
|
668
|
+
|
|
669
|
+
var line = string.String.with_capacity(name_str.len + 64)
|
|
670
|
+
defer line.release()
|
|
671
|
+
|
|
672
|
+
var escaped_name = html_escape(name_str)
|
|
673
|
+
defer escaped_name.release()
|
|
674
|
+
|
|
675
|
+
if is_directory:
|
|
676
|
+
line.append("<a href=\"")
|
|
677
|
+
line.append(escaped_name.as_str())
|
|
678
|
+
line.append("/\">")
|
|
679
|
+
line.append(escaped_name.as_str())
|
|
680
|
+
line.append("/</a>")
|
|
681
|
+
else:
|
|
682
|
+
line.append("<a href=\"")
|
|
683
|
+
line.append(escaped_name.as_str())
|
|
684
|
+
line.append("\">")
|
|
685
|
+
line.append(escaped_name.as_str())
|
|
686
|
+
line.append("</a>")
|
|
687
|
+
|
|
688
|
+
line.append("\n")
|
|
689
|
+
body.append(line.as_str())
|
|
690
|
+
|
|
691
|
+
index += 1
|
|
692
|
+
|
|
693
|
+
body.append("</pre>\n<hr>\n</body>\n</html>")
|
|
694
|
+
|
|
695
|
+
let body_span = text.as_byte_span(body.as_str())
|
|
696
|
+
await send_file_response(stream, 200, "OK", "text/html; charset=utf-8", body_span, head_only)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
async function handle_connection(stream: net.TcpStream, serve_dir: str) -> void:
|
|
700
|
+
var conn = stream
|
|
701
|
+
defer conn.release()
|
|
702
|
+
|
|
703
|
+
var buffer = vec.Vec[ubyte].with_capacity(READ_BUFFER_SIZE)
|
|
704
|
+
defer buffer.release()
|
|
705
|
+
|
|
706
|
+
var headers_done = false
|
|
707
|
+
while not headers_done:
|
|
708
|
+
var chunk = await conn.read_once(READ_BUFFER_SIZE) else:
|
|
709
|
+
return
|
|
710
|
+
buffer.append_span(chunk.as_span())
|
|
711
|
+
chunk.release()
|
|
712
|
+
headers_done = has_header_end(buffer.as_span())
|
|
713
|
+
|
|
714
|
+
let raw = buffer.as_span()
|
|
715
|
+
let header_text = text.utf8_byte_span_as_str(raw) else:
|
|
716
|
+
await send_error(conn, 400, "Bad Request")
|
|
717
|
+
return
|
|
718
|
+
|
|
719
|
+
let first_line_end = find_byte_from_str(header_text, 10, 0)
|
|
720
|
+
var request_line = header_text.slice(0, first_line_end)
|
|
721
|
+
if request_line.ends_with("\r"):
|
|
722
|
+
request_line = header_text.slice(0, request_line.len - 1)
|
|
723
|
+
|
|
724
|
+
let method_end = find_byte_from_str(request_line, 32, 0)
|
|
725
|
+
if method_end >= request_line.len:
|
|
726
|
+
await send_error(conn, 400, "Bad Request")
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
let method = request_line.slice(0, method_end)
|
|
730
|
+
|
|
731
|
+
var path_start = method_end + 1
|
|
732
|
+
while path_start < request_line.len and request_line.byte_at(path_start) == 32:
|
|
733
|
+
path_start += 1
|
|
734
|
+
|
|
735
|
+
let path_end = find_byte_from_str(request_line, 32, path_start)
|
|
736
|
+
var url_path = request_line.slice(path_start, path_end - path_start)
|
|
737
|
+
|
|
738
|
+
if not method.equal("GET") and not method.equal("HEAD"):
|
|
739
|
+
await send_error(conn, 405, "Method Not Allowed")
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
let url_prefix = url_path
|
|
743
|
+
var path_is_root = false
|
|
744
|
+
if url_path.starts_with("/"):
|
|
745
|
+
let stripped = url_path.slice(1, url_path.len - 1)
|
|
746
|
+
if stripped.len == 0:
|
|
747
|
+
path_is_root = true
|
|
748
|
+
url_path = "index.html"
|
|
749
|
+
else:
|
|
750
|
+
url_path = stripped
|
|
751
|
+
|
|
752
|
+
var file_path = path_ops.join(serve_dir, url_path)
|
|
753
|
+
defer file_path.release()
|
|
754
|
+
|
|
755
|
+
let content_result = fs.read_bytes(file_path.as_str())
|
|
756
|
+
match content_result:
|
|
757
|
+
Result.success as content_ok:
|
|
758
|
+
var content = content_ok.value
|
|
759
|
+
defer content.release()
|
|
760
|
+
|
|
761
|
+
let mime = guess_mime(file_path.as_str())
|
|
762
|
+
await send_file_response(conn, 200, "OK", mime, content.as_span(), method.equal("HEAD"))
|
|
763
|
+
Result.failure:
|
|
764
|
+
if fs.is_directory(file_path.as_str()):
|
|
765
|
+
await serve_directory_index(conn, file_path.as_str(), url_prefix, method.equal("HEAD"))
|
|
766
|
+
else if path_is_root:
|
|
767
|
+
await serve_directory_index(conn, serve_dir, url_prefix, method.equal("HEAD"))
|
|
768
|
+
else:
|
|
769
|
+
await send_error(conn, 404, "Not Found")
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
function find_byte_from_str(data: str, target: ubyte, start: ptr_uint) -> ptr_uint:
|
|
773
|
+
var index = start
|
|
774
|
+
while index < data.len:
|
|
775
|
+
if data.byte_at(index) == target:
|
|
776
|
+
return index
|
|
777
|
+
index += 1
|
|
778
|
+
return data.len
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
async function main(args: span[str]) -> void:
|
|
782
|
+
var port = parse_port(args)
|
|
783
|
+
if port <= 0:
|
|
784
|
+
port = DEFAULT_PORT
|
|
785
|
+
|
|
786
|
+
let serve_dir = parse_serve_dir(args)
|
|
787
|
+
|
|
788
|
+
var port_str = fmt.to_string_int(port)
|
|
789
|
+
defer port_str.release()
|
|
790
|
+
|
|
791
|
+
stdio.print_format("Serving HTTP on http://0.0.0.0:%d (dir: %s)\n", port, serve_dir)
|
|
792
|
+
|
|
793
|
+
let address = net.ipv4("0.0.0.0", port) else:
|
|
794
|
+
stdio.print_format("failed to resolve address\n")
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
var listener = net.listen(address, 128) else:
|
|
798
|
+
stdio.print_format("failed to listen\n")
|
|
799
|
+
return
|
|
800
|
+
|
|
801
|
+
defer listener.release()
|
|
802
|
+
|
|
803
|
+
while true:
|
|
804
|
+
let accept_result = await listener.accept()
|
|
805
|
+
if accept_result.is_failure():
|
|
806
|
+
var err = accept_result.unwrap_error()
|
|
807
|
+
stdio.print_format("accept error: %s\n", err.message.as_str())
|
|
808
|
+
err.release()
|
|
809
|
+
else:
|
|
810
|
+
await handle_connection(accept_result.unwrap(), serve_dir)
|