http_resp.nx source
↩ module page · 198 lines · 7238 B
1// http_resp.nx -- HTTP/1.1 response generator.
2//
3// Companion to http.nx (request parser). Assembles a valid HTTP
4// response into a caller-mmapped output buffer: status line,
5// headers, body. No internal allocation beyond tiny scratch.
6//
7// Response shape (RFC 7230 §3):
8// HTTP-version SP status-code SP reason-phrase CRLF
9// *header-field CRLF
10// CRLF
11// [ message-body ]
12//
13// Implementation notes:
14// - Content-Length automatically emitted from body_len. Chunked
15// transfer encoding is a future extension.
16// - Connection: close emitted by default (no keep-alive yet).
17// - Server: nishi emitted for sovereignty attribution.
18// - Date header NOT emitted -- caller can pre-compute via
19// timefmt.nx if clock sync matters for their use case.
20//
21// Invariants:
22// HR1 Output buffer bounds-checked at every append; overrun
23// returns a negative error rather than silent truncation.
24// HR2 No internal state; each response is built from scratch.
25// Thread-safe for the caller's own isolation concerns.
26// HR3 Status codes + reason phrases kept in a fixed table; no
27// arbitrary int -> string formatting on the response path.
28// HR4 Content-Length header reflects body_len exactly; no
29// mismatched framing that would confuse HTTP/1.1 clients.
30
31import "syscalls.nx"
32
33const HRR_ERR_OVERFLOW: i64 = -1
34
35// Common HTTP status codes + reason phrases. Callers pass the
36// numeric status; we map to a phrase string. Missing codes
37// fall through to "Unknown Status" which is technically legal
38// HTTP (RFC 7230 §3.1.2: reason phrase is "essentially ignored
39// by the recipient").
40
41func http_reason(code: i64) -> *u8 {
42 if code == 200 { return "OK" }
43 if code == 201 { return "Created" }
44 if code == 204 { return "No Content" }
45 if code == 301 { return "Moved Permanently" }
46 if code == 302 { return "Found" }
47 if code == 304 { return "Not Modified" }
48 if code == 400 { return "Bad Request" }
49 if code == 401 { return "Unauthorized" }
50 if code == 403 { return "Forbidden" }
51 if code == 404 { return "Not Found" }
52 if code == 405 { return "Method Not Allowed" }
53 if code == 413 { return "Payload Too Large" }
54 if code == 429 { return "Too Many Requests" }
55 if code == 500 { return "Internal Server Error" }
56 if code == 501 { return "Not Implemented" }
57 if code == 503 { return "Service Unavailable" }
58 return "Unknown Status"
59}
60
61// Append a null-terminated cstring to out at *pos. Bounds-check
62// against cap. Returns 0 on success, HRR_ERR_OVERFLOW on
63// overflow.
64func http_append_cstr(out: *u8, pos: *i64, cap: i64, s: *u8) -> i64 {
65 let p0: i64 = *pos
66 var i: i64 = 0
67 while s[i] != 0 {
68 if p0 + i >= cap { return HRR_ERR_OVERFLOW }
69 out[p0 + i] = s[i]
70 i = i + 1
71 }
72 *pos = p0 + i
73 return 0
74}
75
76// Append n bytes from src.
77func http_append_bytes(out: *u8, pos: *i64, cap: i64,
78 src: *u8, n: i64) -> i64 {
79 let p0: i64 = *pos
80 if p0 + n > cap { return HRR_ERR_OVERFLOW }
81 var i: i64 = 0
82 while i < n { out[p0 + i] = src[i]; i = i + 1 }
83 *pos = p0 + n
84 return 0
85}
86
87// Append a decimal-formatted i64 (non-negative).
88func http_append_dec(out: *u8, pos: *i64, cap: i64, n: i64) -> i64 {
89 let p0: i64 = *pos
90 if n == 0 {
91 if p0 >= cap { return HRR_ERR_OVERFLOW }
92 out[p0] = 0x30
93 *pos = p0 + 1
94 return 0
95 }
96 let scratch: *u8 = sys_mmap(32)
97 var v: i64 = n
98 var k: i64 = 0
99 while v > 0 {
100 scratch[k] = 0x30 + (v % 10)
101 v = v / 10
102 k = k + 1
103 }
104 if p0 + k > cap { return HRR_ERR_OVERFLOW }
105 var i: i64 = 0
106 while i < k {
107 out[p0 + i] = scratch[k - 1 - i]
108 i = i + 1
109 }
110 *pos = p0 + k
111 return 0
112}
113
114// Emit status line. "HTTP/1.1 <code> <reason>\r\n".
115func http_emit_status(out: *u8, pos: *i64, cap: i64, code: i64) -> i64 {
116 let rc0: i64 = http_append_cstr(out, pos, cap, "HTTP/1.1 ")
117 if rc0 < 0 { return rc0 }
118 let rc1: i64 = http_append_dec(out, pos, cap, code)
119 if rc1 < 0 { return rc1 }
120 let rc2: i64 = http_append_cstr(out, pos, cap, " ")
121 if rc2 < 0 { return rc2 }
122 let rc3: i64 = http_append_cstr(out, pos, cap, http_reason(code))
123 if rc3 < 0 { return rc3 }
124 let rc4: i64 = http_append_cstr(out, pos, cap, "\r\n")
125 return rc4
126}
127
128// Emit "Name: value\r\n". `value` is a null-terminated cstring.
129func http_emit_header(out: *u8, pos: *i64, cap: i64,
130 name: *u8, value: *u8) -> i64 {
131 let rc0: i64 = http_append_cstr(out, pos, cap, name)
132 if rc0 < 0 { return rc0 }
133 let rc1: i64 = http_append_cstr(out, pos, cap, ": ")
134 if rc1 < 0 { return rc1 }
135 let rc2: i64 = http_append_cstr(out, pos, cap, value)
136 if rc2 < 0 { return rc2 }
137 return http_append_cstr(out, pos, cap, "\r\n")
138}
139
140// Emit "Content-Length: N\r\n" with numeric value.
141func http_emit_content_length(out: *u8, pos: *i64, cap: i64,
142 n: i64) -> i64 {
143 let rc0: i64 = http_append_cstr(out, pos, cap, "Content-Length: ")
144 if rc0 < 0 { return rc0 }
145 let rc1: i64 = http_append_dec(out, pos, cap, n)
146 if rc1 < 0 { return rc1 }
147 return http_append_cstr(out, pos, cap, "\r\n")
148}
149
150// Build a complete response: status + minimal headers + body.
151// Emitted headers:
152// Server: nishi
153// Connection: close
154// Content-Type: <content_type> (caller-supplied cstring)
155// Content-Length: <body_len>
156// Returns total bytes written or HRR_ERR_OVERFLOW.
157func http_build_response(out: *u8, cap: i64,
158 status_code: i64,
159 content_type: *u8,
160 body: *u8, body_len: i64) -> i64 {
161 let pos_raw: *u8 = sys_mmap(16)
162 let pos: *i64 = pos_raw as *i64
163 *pos = 0
164
165 let r1: i64 = http_emit_status(out, pos, cap, status_code)
166 if r1 < 0 { return r1 }
167 let r2: i64 = http_emit_header(out, pos, cap, "Server", "nishi")
168 if r2 < 0 { return r2 }
169 let r3: i64 = http_emit_header(out, pos, cap, "Connection", "close")
170 if r3 < 0 { return r3 }
171 let r4: i64 = http_emit_header(out, pos, cap, "Content-Type",
172 content_type)
173 if r4 < 0 { return r4 }
174 let r5: i64 = http_emit_content_length(out, pos, cap, body_len)
175 if r5 < 0 { return r5 }
176 // Blank line terminates headers.
177 let r6: i64 = http_append_cstr(out, pos, cap, "\r\n")
178 if r6 < 0 { return r6 }
179 // Body.
180 if body_len > 0 {
181 let r7: i64 = http_append_bytes(out, pos, cap, body, body_len)
182 if r7 < 0 { return r7 }
183 }
184 return *pos
185}
186
187// Compile-only smoke.
188func main() -> i64 {
189 let buf: *u8 = sys_mmap(512)
190 let body: *u8 = "Hello, Nishi!"
191 let written: i64 = http_build_response(buf, 512, 200,
192 "text/plain", body, 13)
193 if written <= 0 { return 1 }
194 // First 9 bytes should be "HTTP/1.1 ".
195 if buf[0] != 0x48 { return 2 } // 'H'
196 if buf[5] != 0x2F { return 3 } // '/'
197 return 0
198}