src/proxy.bend
bend-kube-runtime@0.2.0.0 · 427 lines · 61 definitions
1import Base
2import bend-kube-runtime@0.2.0.0/src/utf8.bend as U
3import bend-kube-runtime@0.2.0.0/src/base64.bend as B
4import bend-kube-runtime@0.2.0.0/src/config.bend as K
5
6
7
8
9
10
11
12
13
14
15
16
17
18type Proxy is Data:
19 Proxy{host: String, port: U32, auth: String}
20
21
22type Route is Data:
23 Direct{}
24 Via{proxy: Proxy}
25 NoRoute{why: String}
26
27
28
29
30def Ip4.octet.if(+v: U32, ok: Bool) -> Maybe<&2, U32>:
31 match ok:
32 case True{}:
33 Some{v}
34 case False{}:
35 None{}
36
37def Ip4.octet.go(m: Maybe<&2, U32>) -> Maybe<&2, U32>:
38 match m:
39 case Some{+v}:
40 Ip4.octet.if(v, U32.is_le(v, 255))
41 case None{}:
42 None{}
43
44def Ip4.octet(s: String) -> Maybe<&2, U32>:
45 Ip4.octet.go(U32.read(s))
46
47def Ip4.join(a: Maybe<&2, U32>, b: Maybe<&2, U32>, c: Maybe<&2, U32>, d: Maybe<&2, U32>) ->
48 Maybe<&2, U32>:
49 match a b c d:
50 case Some{w} Some{x} Some{y} Some{z}:
51 Some{U32.add(U32.mul(U32.add(U32.mul(U32.add(U32.mul(w, 256), x), 256), y), 256), z)}
52 case _ _ _ _:
53 None{}
54
55def Ip4.parse.go(ps: List<&2, String>) -> Maybe<&2, U32>:
56 match ps:
57 case Con{a, Con{b, Con{c, Con{d, Nil{}}}}}:
58 Ip4.join(Ip4.octet(a), Ip4.octet(b), Ip4.octet(c), Ip4.octet(d))
59 case _:
60 None{}
61
62
63def Ip4.parse(s: String) -> Maybe<&2, U32>:
64 Ip4.parse.go(String.split(s, '.'))
65
66def Ip4.is_loopback(m: Maybe<&2, U32>) -> Bool:
67 match m:
68 case Some{v}:
69 U32.is_eq(U32.shrn(v, 24n), 127)
70 case None{}:
71 False{}
72
73
74def Ip4.mask.go(+bits: U32, ok: Bool) -> Maybe<&2, U32>:
75 match ok:
76 case True{}:
77 Some{U32.shln(4294967295, U32.to_nat(U32.sub(32, bits)))}
78 case False{}:
79 None{}
80
81def Ip4.mask(+bits: U32) -> Maybe<&2, U32>:
82 Ip4.mask.go(bits, U32.is_le(bits, 32))
83
84def Ip4.in(h: Maybe<&2, U32>, n: Maybe<&2, U32>, m: Maybe<&2, U32>) -> Bool:
85 match h n m:
86 case Some{x} Some{y} Some{+k}:
87 U32.is_eq(U32.and(x, k), U32.and(y, k))
88 case _ _ _:
89 False{}
90
91def Ip4.mask.of(m: Maybe<&2, U32>) -> Maybe<&2, U32>:
92 match m:
93 case Some{+b}:
94 Ip4.mask(b)
95 case None{}:
96 None{}
97
98def Ip4.cidr.go(h: String, parts: List<&2, String>) -> Bool:
99 match parts:
100 case Con{n, Con{b, Nil{}}}:
101 Ip4.in(Ip4.parse(h), Ip4.parse(n), Ip4.mask.of(U32.read(b)))
102 case _:
103 False{}
104
105
106def Ip4.cidr(h: String, block: String) -> Bool:
107 Ip4.cidr.go(h, String.split(block, '/'))
108
109
110
111
112
113
114
115
116
117
118type Ent is Data:
119 Ent{host: String, port: String}
120
121def Ent.of.go(parts: List<&2, String>, +item: String) -> Ent:
122 match parts:
123 case Con{h, Con{p, Nil{}}}:
124 Ent{h, p}
125 case _:
126 Ent{item, ""}
127
128def Ent.of(+item: String) -> Ent:
129 Ent.of.go(String.split(item, ':'), item)
130
131def NoProxy.star(e: String) -> String:
132 match e:
133 case SCon{'*', SCon{'.', t}}:
134 SCon{'.', t}
135 case x:
136 x
137
138def NoProxy.name.go(+e: String, +host: String, dot: Bool) -> Bool:
139 match dot:
140 case True{}:
141 String.ends_with(host, e)
142 case False{}:
143 Bool.or(String.eq(host, e), String.ends_with(host, "." ++ e))
144
145def NoProxy.name(+e: String, +host: String) -> Bool:
146 NoProxy.name.go(e, host, String.starts_with(e, "."))
147
148def NoProxy.port_ok(+want: String, +port: U32) -> Bool:
149 Bool.or(String.is_empty(want), String.eq(want, U32.show(port)))
150
151def NoProxy.ent(e: Ent, +host: String, +port: U32) -> Bool:
152 match e:
153 case Ent{+h, +p}:
154 Bool.and(NoProxy.port_ok(p, port),
155 Bool.or(Ip4.cidr(host, h), NoProxy.name(NoProxy.star(h), host)))
156
157def NoProxy.hit.go(+host: String, +port: U32, +item: String, star: Bool) -> Bool:
158 match star:
159 case True{}:
160 True{}
161 case False{}:
162 NoProxy.ent(Ent.of(item), host, port)
163
164def NoProxy.hit(+host: String, +port: U32, +item: String) -> Bool:
165 NoProxy.hit.go(host, port, item, String.eq(item, "*"))
166
167def NoProxy.any(xs: List<&2, String>, +host: String, +port: U32) -> Bool:
168 match xs:
169 case Nil{}:
170 False{}
171 case Con{x, t}:
172 Bool.or(NoProxy.hit(host, port, x), NoProxy.any(t, host, port))
173
174def NoProxy.commas(s: String) -> String:
175 match s:
176 case SNil{}:
177 SNil{}
178 case SCon{' ', t}:
179 SCon{',', NoProxy.commas(t)}
180 case SCon{c, t}:
181 SCon{c, NoProxy.commas(t)}
182
183
184def NoProxy.bypass(no: String, +host: String, +port: U32) -> Bool:
185 NoProxy.any(String.split(String.to_lower(NoProxy.commas(no)), ','), host, port)
186
187
188
189
190def Pct.hex.go(+c: U32, d: Bool, up: Bool, low: Bool) -> U32:
191 match d up low:
192 case True{} _ _:
193 U32.sub(c, 48)
194 case _ True{} _:
195 U32.sub(c, 55)
196 case _ _ True{}:
197 U32.sub(c, 87)
198 case _ _ _:
199 16
200
201
202def Pct.hex(+c: U32) -> U32:
203 Pct.hex.go(c, Bool.and(U32.is_ge(c, 48), U32.is_le(c, 57)),
204 Bool.and(U32.is_ge(c, 65), U32.is_le(c, 70)), Bool.and(U32.is_ge(c, 97), U32.is_le(c, 102)))
205
206def Pct.esc.if(+a: Char, +b: Char, +hi: U32, +lo: U32, rest: List<&2, U32>, ok: Bool) ->
207 List<&2, U32>:
208 match ok:
209 case True{}:
210 Con{U32.add(U32.mul(hi, 16), lo), rest}
211 case False{}:
212 Con{37, U.Utf8.put(Char.to_u32(a), U.Utf8.put(Char.to_u32(b), rest))}
213
214def Pct.esc(+a: Char, +b: Char, rest: List<&2, U32>) -> List<&2, U32>:
215 +hi = Pct.hex(Char.to_u32(a))
216 +lo = Pct.hex(Char.to_u32(b))
217 Pct.esc.if(a, b, hi, lo, rest, Bool.and(U32.is_lt(hi, 16), U32.is_lt(lo, 16)))
218
219
220
221def Pct.decode(s: String) -> List<&2, U32>:
222 match s:
223 case SNil{}:
224 Nil{}
225 case SCon{'%', SCon{a, SCon{b, t}}}:
226 Pct.esc(a, b, Pct.decode(t))
227 case SCon{c, t}:
228 U.Utf8.put(Char.to_u32(c), Pct.decode(t))
229
230def Proxy.parse.fin(+h: String, +p: U32, auth: String, empty: Bool) -> Route:
231 match empty:
232 case True{}:
233 NoRoute{"HTTPS_PROXY names no host"}
234 case False{}:
235 Via{Proxy{h, p, auth}}
236
237def Proxy.parse.hp(hp: String & U32, auth: String) -> Route:
238 match hp:
239 case (+h, +p):
240 Proxy.parse.fin(h, p, auth, String.is_empty(h))
241
242
243
244def Proxy.parse.at(rs: List<&2, String>) -> Route:
245 match rs:
246 case Con{hp, Nil{}}:
247 Proxy.parse.hp(K.Url.host_port(hp, 80), "")
248 case Con{hp, us}:
249 Proxy.parse.hp(K.Url.host_port(hp, 80),
250 B.B64.encode(Pct.decode(String.join(List.reverse(&2, String, us), "@"))))
251 case Nil{}:
252 NoRoute{"HTTPS_PROXY names no host"}
253
254def Proxy.parse.auth(ap: String & String) -> Route:
255 match ap:
256 case (a, p):
257 Proxy.parse.at(List.reverse(&2, String, String.split(a, '@')))
258
259def Proxy.parse.go(+u: String, http: Bool, other: Bool) -> Route:
260 match http other:
261 case True{} _:
262 Proxy.parse.auth(K.Url.split_path(String.drop(u, 7n), ""))
263 case False{} True{}:
264 NoRoute{"HTTPS_PROXY: only http:// proxies are supported"}
265 case False{} False{}:
266 Proxy.parse.auth(K.Url.split_path(u, ""))
267
268
269
270def Proxy.parse(+u: String) -> Route:
271 Proxy.parse.go(u, String.starts_with(String.to_lower(u), "http://"), String.contains(u, "://"))
272
273
274
275
276def Proxy.route.url(+url: String, empty: Bool) -> Route:
277 match empty:
278 case True{}:
279 Direct{}
280 case False{}:
281 Proxy.parse(String.trim(url))
282
283def Proxy.route.no(+url: String, by: Bool) -> Route:
284 match by:
285 case True{}:
286 Direct{}
287 case False{}:
288 Proxy.route.url(url, String.is_empty(String.trim(url)))
289
290def Proxy.route.local(+url: String, no: String, +host: String, +port: U32, local: Bool) ->
291 Route:
292 match local:
293 case True{}:
294 Direct{}
295 case False{}:
296 Proxy.route.no(url, NoProxy.bypass(no, host, port))
297
298def Proxy.is_local(+host: String) -> Bool:
299 Bool.or(Bool.or(String.eq(host, "localhost"), String.eq(host, "::1")),
300 Ip4.is_loopback(Ip4.parse(host)))
301
302
303
304
305def Proxy.route(+url: String, no: String, host: String, +port: U32) -> Route:
306 +h = String.to_lower(host)
307 Proxy.route.local(url, no, h, port, Proxy.is_local(h))
308
309
310
311
312def Proxy.target.go(+h: String, +p: U32, v6: Bool) -> String:
313 match v6:
314 case True{}:
315 "[" ++ h ++ "]:" ++ U32.show(p)
316 case False{}:
317 h ++ ":" ++ U32.show(p)
318
319def Proxy.target(+h: String, +p: U32) -> String:
320 Proxy.target.go(h, p, String.contains(h, ":"))
321
322def Proxy.auth_line.go(+auth: String, none: Bool) -> String:
323 match none:
324 case True{}:
325 ""
326 case False{}:
327 "Proxy-Authorization: Basic " ++ auth ++ "\r\n"
328
329
330def Proxy.connect(+host: String, +port: U32, +auth: String) -> String:
331 +t = Proxy.target(host, port)
332 "CONNECT " ++ t ++ " HTTP/1.1\r\nHost: " ++ t ++ "\r\n"
333 ++ Proxy.auth_line.go(auth, String.is_empty(auth)) ++ "\r\n"
334
335
336
337
338
339
340
341
342
343type Tunnel is Data:
344 TMore{head: List<&2, U32>}
345 TOpen{}
346 TFail{why: String}
347
348def Tunnel.init() -> Tunnel:
349 TMore{Nil{}}
350
351def Tunnel.is_end(+h: List<&2, U32>) -> Bool:
352 match h:
353 case Con{+a, Con{+b, Con{+c, Con{+d, t}}}}:
354 Bool.and(Bool.and(U32.is_eq(a, 10), U32.is_eq(b, 13)), Bool.and(U32.is_eq(c, 10), U32.is_eq(d, 13)))
355 case _:
356 False{}
357
358def Tunnel.line.if(+b: U32, rest: List<&2, U32>, cr: Bool) -> List<&2, U32>:
359 match cr:
360 case True{}:
361 Nil{}
362 case False{}:
363 Con{b, rest}
364
365
366def Tunnel.line(bs: List<&2, U32>) -> List<&2, U32>:
367 match bs:
368 case Nil{}:
369 Nil{}
370 case Con{+b, t}:
371 Tunnel.line.if(b, Tunnel.line(t), U32.is_eq(b, 13))
372
373def Tunnel.verdict.go(+line: String, ok: Bool) -> Tunnel:
374 match ok:
375 case True{}:
376 TOpen{}
377 case False{}:
378 TFail{"proxy refused CONNECT: " ++ line}
379
380def Tunnel.verdict(+line: String) -> Tunnel:
381 Tunnel.verdict.go(line, Bool.and(String.starts_with(line, "HTTP/1."),
382 String.starts_with(String.drop(line, 9n), "200")))
383
384def Tunnel.done(h: List<&2, U32>, rest: List<&2, U32>) -> Tunnel:
385 match rest:
386 case Nil{}:
387 Tunnel.verdict(U.Utf8.decode_all(Tunnel.line(List.reverse(&2, U32, h))))
388 case Con{b, t}:
389 TFail{"proxy sent bytes before the TLS handshake"}
390
391def Tunnel.push(bs: List<&2, U32>, +h: List<&2, U32>, end: Bool) -> Tunnel:
392 match bs end:
393 case rest True{}:
394 Tunnel.done(h, rest)
395 case Nil{} False{}:
396 TMore{h}
397 case Con{+b, t} False{}:
398 Tunnel.push(t, Con{b, h}, Tunnel.is_end(Con{b, h}))
399
400def Tunnel.cap.go(+h: List<&2, U32>, long: Bool) -> Tunnel:
401 match long:
402 case True{}:
403 TFail{"proxy answer too long"}
404 case False{}:
405 TMore{h}
406
407def Tunnel.cap(t: Tunnel) -> Tunnel:
408 match t:
409 case TMore{+h}:
410 Tunnel.cap.go(h, Nat.is_gt(List.length(&2, U32, h), 16384n))
411 case x:
412 x
413
414def Tunnel.more(+h: List<&2, U32>, bs: List<&2, U32>, closed: Bool) -> Tunnel:
415 match closed:
416 case True{}:
417 TFail{"proxy closed the connection before answering CONNECT"}
418 case False{}:
419 Tunnel.cap(Tunnel.push(bs, h, False{}))
420
421
422def Tunnel.feed(t: Tunnel, +bs: List<&2, U32>) -> Tunnel:
423 match t:
424 case TMore{+h}:
425 Tunnel.more(h, bs, List.is_empty(&2, U32, bs))
426 case x:
427 x