nx_http_redirect_daemon.nx source
↩ module page · 148 lines · 5888 B
1// nx_http_redirect_daemon.nx -- sovereign plain-HTTP listener that
2// 301-redirects everything to HTTPS. Pure Nishi, no TLS, no libc.
3//
4// Operator 2026-05-28: "redirect the http to it" -- port 80 must send
5// browsers to https:// instead of landing on the Synology DSM (:5000).
6//
7// Listens on 0.0.0.0:8080 (GL forwards public :80 here). For each
8// request it reads the start line + Host header and replies:
9// HTTP/1.0 301 Moved Permanently
10// Location: https://<host><path>
11// preserving the requested path so deep links survive the upgrade.
12//
13// Public-exposure hardened (SO_RCVTIMEO/SNDTIMEO) + constant memory
14// (all buffers hoisted out of the accept loop) -- same discipline as
15// nx_sites_daemon (PREVENT: no sys_mmap in the loop).
16
17import "nx_syscalls.nx"
18import "nx_http_server.nx"
19
20const NX_RD_PORT: i64 = 8080
21const NX_RD_BUDGET: i64 = 100000000
22
23func rd_find(hay: *u8, hlen: i64, needle: *u8, nlen: i64) -> i64 {
24 if nlen <= 0 { return 0 - 1 }
25 if hlen < nlen { return 0 - 1 }
26 var i: i64 = 0
27 let last: i64 = hlen - nlen
28 while i <= last {
29 var j: i64 = 0
30 var ok: i64 = 1
31 while j < nlen {
32 if hay[i + j] != needle[j] { ok = 0; j = nlen } else { j = j + 1 }
33 }
34 if ok == 1 { return i }
35 i = i + 1
36 }
37 return 0 - 1
38}
39
40func main() -> i64 {
41 let addr_buf: *u8 = sys_mmap(16)
42 nx_http_server_addr_any(addr_buf, NX_RD_PORT)
43 let lv: *i64 = (sys_mmap(8)) as *i64
44 let lfd: i64 = nx_http_server_listen(addr_buf, 16, lv)
45 if lfd < 0 { return 4 }
46 sys_write(1, "nishi http-redirect daemon: 0.0.0.0:8080 -> 301 https\n" as *u8, 54)
47
48 // Constant memory: hoist all per-connection buffers.
49 let av: *i64 = (sys_mmap(8)) as *i64
50 let req: *u8 = sys_mmap(8192)
51 let resp: *u8 = sys_mmap(8192)
52 let host: *u8 = sys_mmap(256)
53 let ka: *u8 = sys_mmap(512) // ACME HTTP-01 key authorization buffer
54
55 var served: i64 = 0
56 while served < NX_RD_BUDGET {
57 let cfd: i64 = nx_http_server_accept_one(lfd, av)
58 if cfd < 0 { served = served + 1; continue }
59 sys_set_socket_timeout(cfd, 10)
60
61 let n: i64 = sys_read(cfd, req, 8191)
62 if n > 0 {
63 req[n] = 0
64 // --- extract path from the request line: "GET <path> HTTP/.." ---
65 var ps: i64 = 0
66 while ps < n {
67 if req[ps] == 0x20 { ps = ps + 1; break } // first space after method
68 ps = ps + 1
69 }
70 var pe: i64 = ps
71 while pe < n {
72 if req[pe] == 0x20 { break } // space before HTTP/x
73 pe = pe + 1
74 }
75 // --- ACME HTTP-01: serve keyAuth at /.well-known/acme-challenge/* ---
76 // (RFC 8555 ยง8.3) instead of redirecting, so Let's Encrypt can
77 // validate domain control over plain HTTP on port 80.
78 var is_acme: i64 = 0
79 let wk: *u8 = "/.well-known/acme-challenge/" as *u8
80 if (pe - ps) >= 27 {
81 var m: i64 = 1
82 var z: i64 = 0
83 while z < 27 { if req[ps + z] != wk[z] { m = 0; z = 27 } else { z = z + 1 } }
84 is_acme = m
85 }
86 if is_acme == 1 {
87 let kfd: i64 = sys_openat_rd("/volume1/homes/elderwesto/nishihost/acme-challenge/keyauth\x00" as *u8)
88 if kfd > 0 {
89 let kn: i64 = sys_read(kfd, ka, 511)
90 sys_close(kfd)
91 if kn > 0 {
92 var rw: i64 = 0
93 let hh: *u8 = "HTTP/1.0 200 OK\r\nContent-Type: application/octet-stream\r\nConnection: close\r\n\r\n" as *u8
94 var ha: i64 = 0
95 while hh[ha] != 0 { resp[rw] = hh[ha]; rw = rw + 1; ha = ha + 1 }
96 var kk: i64 = 0
97 while kk < kn { resp[rw] = ka[kk]; rw = rw + 1; kk = kk + 1 }
98 sys_write(cfd, resp, rw)
99 }
100 }
101 sys_close(cfd)
102 served = served + 1
103 continue
104 }
105 // --- extract Host header value ---
106 var host_n: i64 = 0
107 let hpos: i64 = rd_find(req, n, "Host: " as *u8, 6)
108 if hpos >= 0 {
109 var k: i64 = hpos + 6
110 while k < n {
111 let c: i64 = req[k] as i64
112 if c == 0x0D { break } // CR
113 if c == 0x0A { break } // LF
114 if host_n < 255 { host[host_n] = req[k] as u8; host_n = host_n + 1 }
115 k = k + 1
116 }
117 }
118 if host_n == 0 {
119 let dh: *u8 = "andelinwest.com" as *u8
120 var d: i64 = 0
121 while d < 15 { host[d] = dh[d]; d = d + 1 }
122 host_n = 15
123 }
124 // --- build the 301 ---
125 var w: i64 = 0
126 let h1: *u8 = "HTTP/1.0 301 Moved Permanently\r\nLocation: https://" as *u8
127 var a: i64 = 0
128 while h1[a] != 0 { resp[w] = h1[a]; w = w + 1; a = a + 1 }
129 var hi: i64 = 0
130 while hi < host_n { resp[w] = host[hi]; w = w + 1; hi = hi + 1 }
131 // path (from request line); fall back to "/"
132 if pe > ps {
133 var pp: i64 = ps
134 while pp < pe { resp[w] = req[pp]; w = w + 1; pp = pp + 1 }
135 } else {
136 resp[w] = 0x2F as u8; w = w + 1
137 }
138 let h2: *u8 = "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n" as *u8
139 var b: i64 = 0
140 while h2[b] != 0 { resp[w] = h2[b]; w = w + 1; b = b + 1 }
141 sys_write(cfd, resp, w)
142 }
143 sys_close(cfd)
144 served = served + 1
145 }
146 sys_close(lfd)
147 return 0
148}