nx_pages_http_redirect.nx source
↩ module page · 153 lines · 5288 B
1// nx_pages_http_redirect.nx -- D1.4 of NISHI_DEPLOY_ROADMAP.
2//
3// Tiny daemon that listens on :80 and 301-redirects every request to
4// the same Host + path on https://. This is the partner of D1.3's
5// nx_pages_https_daemon -- together they replace nginx's TLS termination
6// + ACME http-01 challenge surface (when D1.5 lands, this daemon also
7// serves /.well-known/acme-challenge/ for renewals).
8//
9// V1 behaviour:
10// - GET / -> 301 Location: https://<Host>/
11// - GET /foo/bar -> 301 Location: https://<Host>/foo/bar
12// - No Host header -> 301 Location: https://nishifamily.com/<path>
13//
14// V2 hook (D1.5): if path starts with /.well-known/acme-challenge/,
15// serve the matching token from a config-driven directory instead of
16// redirecting. Tracked as TODO in this file.
17
18import "nx_syscalls.nx"
19import "nx_http.nx"
20import "nx_http_server.nx"
21const NX_MAGIC_2048: i64 = 2048
22
23const NX_REDIRECT_PORT: i64 = 80
24const NX_REDIRECT_BACKLOG: i64 = 64
25const NX_REDIRECT_REQ_CAP: i64 = 8192
26const NX_REDIRECT_OUT_CAP: i64 = 8192
27
28const NX_REDIRECT_OK: i64 = 0
29const NX_REDIRECT_ERR_LISTEN: i64 = 1
30const NX_REDIRECT_ERR_PARSE: i64 = 2
31
32// Default host used when the request has no Host header. Production
33// SHOULD always have a Host header; this is the defensive default.
34// D1.6 will make it configurable. Returned by a function since
35// NishiLang const only accepts integer literals.
36func _redirect_default_host() -> *u8 { return "nishifamily.com" as *u8 }
37
38// Build a 301 redirect response into `out_buf`. Writes the response
39// length to *out_n. Format:
40// HTTP/1.1 301 Moved Permanently\r\n
41// Location: https://<host>/<path>\r\n
42// Content-Length: 0\r\n
43// Connection: close\r\n
44// \r\n
45//
46// host_buf+off..off+len = the Host bytes from the request (or default
47// host fallback bytes if not present). path_buf+path_off..+path_len
48// = the request path.
49func _build_301(
50 out_buf: *u8, out_cap: i64,
51 host_buf: *u8, host_off: i64, host_len: i64,
52 path_buf: *u8, path_off: i64, path_len: i64,
53 out_n: *i64
54) -> i64 {
55 var w: i64 = 0
56 let line1: *u8 = "HTTP/1.1 301 Moved Permanently\r\nLocation: https://" as *u8
57 var i: i64 = 0
58 while line1[i] != 0 { out_buf[w] = line1[i]; w = w + 1; i = i + 1 }
59
60 // Write Host bytes.
61 var hi: i64 = 0
62 while hi < host_len {
63 out_buf[w] = host_buf[host_off + hi]
64 w = w + 1
65 hi = hi + 1
66 }
67
68 // Write path bytes (already starts with / per HTTP/1.1 request-line).
69 var pi: i64 = 0
70 while pi < path_len {
71 out_buf[w] = path_buf[path_off + pi]
72 w = w + 1
73 pi = pi + 1
74 }
75
76 let line2: *u8 = "\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" as *u8
77 var j: i64 = 0
78 while line2[j] != 0 { out_buf[w] = line2[j]; w = w + 1; j = j + 1 }
79
80 out_n[0] = w
81 return 0
82}
83
84// Per-connection: read request, parse, send 301, close.
85func _redirect_serve_one(cfd: i64) -> i64 {
86 let req_buf: *u8 = sys_mmap(NX_REDIRECT_REQ_CAP)
87 let n: i64 = sys_read(cfd, req_buf, NX_REDIRECT_REQ_CAP)
88 if n <= 0 {
89 sys_close(cfd)
90 return NX_REDIRECT_ERR_PARSE
91 }
92
93 let req: *HttpRequest = sys_mmap(NX_MAGIC_2048) as *HttpRequest
94 let parse_rc: i64 = http_parse(req_buf, n, req)
95 if parse_rc != 0 {
96 sys_close(cfd)
97 return NX_REDIRECT_ERR_PARSE
98 }
99
100 // Look up Host header value.
101 let host_off: *i64 = sys_mmap(8) as *i64
102 let host_len: *i64 = sys_mmap(8) as *i64
103 let has_host: i64 = http_get_header(req_buf, req, "Host" as *u8,
104 host_off, host_len)
105
106 let out_buf: *u8 = sys_mmap(NX_REDIRECT_OUT_CAP)
107 let out_n: *i64 = sys_mmap(8) as *i64
108 if has_host == 1 {
109 _build_301(out_buf, NX_REDIRECT_OUT_CAP,
110 req_buf, host_off[0], host_len[0],
111 req_buf, req.path_off, req.path_len,
112 out_n)
113 }
114 if has_host != 1 {
115 // Fallback: nishifamily.com as the Host.
116 let dh: *u8 = _redirect_default_host()
117 var dh_len: i64 = 0
118 while dh[dh_len] != 0 { dh_len = dh_len + 1 }
119 _build_301(out_buf, NX_REDIRECT_OUT_CAP,
120 dh, 0 as i64, dh_len,
121 req_buf, req.path_off, req.path_len,
122 out_n)
123 }
124
125 nx_http_server_send_response(cfd, out_buf, out_n[0])
126 return NX_REDIRECT_OK
127}
128
129// Entry point: listen :80, accept loop.
130func nx_pages_http_redirect_daemon_run() -> i64 {
131 let addr_buf: *u8 = sys_mmap(16)
132 nx_http_server_addr_any(addr_buf, NX_REDIRECT_PORT)
133 let verdict: *i64 = sys_mmap(8) as *i64
134 let listen_fd: i64 = nx_http_server_listen(addr_buf, NX_REDIRECT_BACKLOG, verdict)
135 if listen_fd < 0 { return NX_REDIRECT_ERR_LISTEN }
136
137 let banner: *u8 = "nx_pages_http_redirect: listening on :80 (-> https://)\n" as *u8
138 var bn: i64 = 0
139 while banner[bn] != 0 { bn = bn + 1 }
140 sys_write(1 as i64, banner, bn)
141
142 var running: i64 = 1
143 while running == 1 {
144 let cfd: i64 = nx_http_server_accept_one(listen_fd, verdict)
145 if cfd >= 0 { _redirect_serve_one(cfd) }
146 }
147 sys_close(listen_fd)
148 return NX_REDIRECT_OK
149}
150
151func main() -> i64 {
152 return nx_pages_http_redirect_daemon_run()
153}