nx_cors.nx source
↩ module page · 175 lines · 6146 B
1// cors.nx -- build CORS response headers.
2//
3// Fetch / XHR from a browser to a cross-origin server triggers
4// CORS (Cross-Origin Resource Sharing). Browser sends a preflight
5// OPTIONS request with Origin + Access-Control-Request-Method +
6// Access-Control-Request-Headers. Server answers with Access-
7// Control-Allow-* headers indicating what's permitted.
8//
9// Spec: Fetch Living Standard §Cors (supersedes the W3C CORS rec).
10//
11// Typical minimal response for a permissive public API:
12// Access-Control-Allow-Origin: *
13// Access-Control-Allow-Methods: GET, POST, OPTIONS
14// Access-Control-Allow-Headers: Content-Type, Authorization
15// Access-Control-Max-Age: 86400
16//
17// For credentialed requests (cookies / auth), Allow-Origin MUST
18// echo the specific origin (not '*') and Allow-Credentials MUST
19// be true. We expose both modes.
20//
21// Invariants:
22// CO1 Stream builder; caller supplies an output buffer + offset.
23// CO2 Headers include trailing CRLF; caller concatenates them
24// after the HTTP status line.
25
26// nx_safety_envelope:
27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
28// sil_target: SIL1
29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
30// verdict: NOT_YET_EVALUATED
31
32import "nx_syscalls.nx"
33const CORS_MAGIC_86400: i64 = 86400
34const CORS_MAGIC_1024: i64 = 1024
35
36const CORS_ERR_SHORT: i64 = -1
37
38func co_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 {
39 if off + n > cap { return CORS_ERR_SHORT }
40 var i: i64 = 0
41 while i < n {
42 out[off + i] = src[i]
43 i = i + 1
44 }
45 return off + n
46}
47
48// Access-Control-Allow-Origin header. Pass "*" for wildcard or
49// a specific origin URL for credentialed endpoints.
50func cors_allow_origin(out: *u8, cap: i64, off: i64,
51 origin: *u8, origin_len: i64) -> i64 {
52 var cur: i64 = off
53 cur = co_put(out, cap, cur, "Access-Control-Allow-Origin: ", 29)
54 if cur < 0 { return cur }
55 cur = co_put(out, cap, cur, origin, origin_len)
56 if cur < 0 { return cur }
57 cur = co_put(out, cap, cur, "\r\n", 2)
58 return cur
59}
60
61// Allow specific methods -- caller supplies comma-separated list.
62// e.g. "GET, POST, PUT, DELETE, OPTIONS".
63func cors_allow_methods(out: *u8, cap: i64, off: i64,
64 methods: *u8, methods_len: i64) -> i64 {
65 var cur: i64 = off
66 cur = co_put(out, cap, cur, "Access-Control-Allow-Methods: ", 30)
67 if cur < 0 { return cur }
68 cur = co_put(out, cap, cur, methods, methods_len)
69 if cur < 0 { return cur }
70 cur = co_put(out, cap, cur, "\r\n", 2)
71 return cur
72}
73
74// Allow-Headers: comma-separated list of request headers the
75// client is permitted to send.
76func cors_allow_headers(out: *u8, cap: i64, off: i64,
77 headers: *u8, headers_len: i64) -> i64 {
78 var cur: i64 = off
79 cur = co_put(out, cap, cur, "Access-Control-Allow-Headers: ", 30)
80 if cur < 0 { return cur }
81 cur = co_put(out, cap, cur, headers, headers_len)
82 if cur < 0 { return cur }
83 cur = co_put(out, cap, cur, "\r\n", 2)
84 return cur
85}
86
87// Expose-Headers: the response headers the client's JS can read
88// (everything outside the CORS-safelisted set needs opting-in).
89func cors_expose_headers(out: *u8, cap: i64, off: i64,
90 headers: *u8, headers_len: i64) -> i64 {
91 var cur: i64 = off
92 cur = co_put(out, cap, cur, "Access-Control-Expose-Headers: ", 31)
93 if cur < 0 { return cur }
94 cur = co_put(out, cap, cur, headers, headers_len)
95 if cur < 0 { return cur }
96 cur = co_put(out, cap, cur, "\r\n", 2)
97 return cur
98}
99
100// Allow cookies / Authorization to cross origins. Requires
101// Allow-Origin to be a specific origin (not '*').
102func cors_allow_credentials(out: *u8, cap: i64, off: i64) -> i64 {
103 return co_put(out, cap, off,
104 "Access-Control-Allow-Credentials: true\r\n", 40)
105}
106
107// Max-Age: how long (seconds) browsers may cache the preflight
108// response. Browsers cap to their own max (Chrome: 7200s, FF:
109// 86400s) regardless.
110func cors_max_age(out: *u8, cap: i64, off: i64, seconds: i64) -> i64 {
111 var cur: i64 = off
112 cur = co_put(out, cap, cur, "Access-Control-Max-Age: ", 24)
113 if cur < 0 { return cur }
114
115 // Emit decimal seconds.
116 if seconds == 0 {
117 if cur + 1 > cap { return CORS_ERR_SHORT }
118 out[cur] = 0x30
119 cur = cur + 1
120 } else {
121 let digits_raw: *u8 = sys_mmap(32)
122 var tmp: i64 = seconds
123 var dn: i64 = 0
124 while tmp > 0 {
125 digits_raw[dn] = 0x30 + (tmp % 10)
126 tmp = tmp / 10
127 dn = dn + 1
128 }
129 if cur + dn > cap { return CORS_ERR_SHORT }
130 var i: i64 = dn - 1
131 while i >= 0 {
132 out[cur] = digits_raw[i]
133 cur = cur + 1
134 i = i - 1
135 }
136 }
137 cur = co_put(out, cap, cur, "\r\n", 2)
138 return cur
139}
140
141// Convenience: emit a permissive public-API preflight response
142// (Allow: *, common methods + headers, 24-hour cache).
143func cors_permissive(out: *u8, cap: i64, off: i64) -> i64 {
144 var cur: i64 = off
145 cur = cors_allow_origin(out, cap, cur, "*", 1)
146 if cur < 0 { return cur }
147 cur = cors_allow_methods(out, cap, cur,
148 "GET, POST, PUT, DELETE, OPTIONS", 31)
149 if cur < 0 { return cur }
150 cur = cors_allow_headers(out, cap, cur,
151 "Content-Type, Authorization", 27)
152 if cur < 0 { return cur }
153 cur = cors_max_age(out, cap, cur, CORS_MAGIC_86400)
154 return cur
155}
156
157// Compile-only smoke.
158func main() -> i64 {
159 let out: *u8 = sys_mmap(CORS_MAGIC_1024)
160 let n: i64 = cors_permissive(out, CORS_MAGIC_1024, 0)
161 if n <= 0 { return 1 }
162
163 // First char is 'A' of \"Access-Control\".
164 if out[0] != 0x41 { return 2 }
165
166 // Credentialed flow: specific origin + credentials.
167 var off: i64 = 0
168 off = cors_allow_origin(out, CORS_MAGIC_1024, off,
169 "https://nishifamily.com", 23)
170 if off < 0 { return 3 }
171 off = cors_allow_credentials(out, CORS_MAGIC_1024, off)
172 if off < 0 { return 4 }
173
174 return 0
175}