1use bstr::ByteSlice;
2use config::{
3 any_err, decorate_callback_name, from_lua_value, get_or_create_module, load_config,
4 serialize_options, CallbackSignature,
5};
6use kumo_server_runtime::available_parallelism;
7use mlua::{Function, Lua, LuaSerdeExt, Value, Variadic};
8use mod_redis::RedisConnKey;
9use serde::{Deserialize, Serialize};
10use std::sync::atomic::AtomicUsize;
11
12pub mod acct;
13pub mod authn_authz;
14pub mod config_handle;
15pub mod diagnostic_logging;
16pub mod disk_space;
17pub mod hashable_weak;
18pub mod http_server;
19pub mod listener;
20pub mod log;
21pub mod log_backlog;
22pub mod nodeid;
23pub mod panic;
24pub mod start;
25pub mod tls_helpers;
26
27pub fn register(lua: &Lua) -> anyhow::Result<()> {
28 for func in [
29 mod_redis::register,
30 data_loader::register,
31 mod_digest::register,
32 mod_encode::register,
33 mod_aws_sigv4::register,
34 cidr_map::register,
35 domain_map::register,
36 mod_amqp::register,
37 mod_filesystem::register,
38 mod_file_type::register,
39 mod_http::register,
40 mod_regex::register,
41 mod_serde::register,
42 mod_sqlite::register,
43 mod_crypto::register,
44 mod_smtp_response_normalize::register,
45 kumo_jsonl::lua::register,
46 mod_counter_series::register,
47 mod_string::register,
48 mod_time::register,
49 mod_dns_resolver::register,
50 mod_kafka::register,
51 mod_memoize::register,
52 mod_mimepart::register,
53 mod_mpsc::register,
54 mod_nats::register,
55 mod_uuid::register,
56 kumo_api_types::shaping::register,
57 regex_set_map::register,
58 crate::authn_authz::register,
59 ] {
60 func(lua)?;
61 }
62
63 let kumo_mod = get_or_create_module(lua, "kumo")?;
64 kumo_mod.set("version", version_info::kumo_version())?;
65
66 fn event_registrar_name(name: &str) -> String {
67 format!("kumomta-event-registrars-{name}")
68 }
69
70 fn register_event_caller(lua: &Lua, name: &str) -> mlua::Result<()> {
73 let decorated_name = event_registrar_name(name);
74 let mut call_stack = vec![];
75 for n in 1.. {
76 match lua.inspect_stack(n, |info| {
77 let source = info.source();
78 format!(
79 "{}:{}",
80 source
81 .short_src
82 .as_ref()
83 .map(|b| b.to_string())
84 .unwrap_or_else(String::new),
85 info.current_line().unwrap_or(0)
86 )
87 }) {
88 Some(info) => {
89 call_stack.push(info);
90 }
91 None => break,
92 }
93 }
94
95 let tbl: Value = lua.named_registry_value(&decorated_name)?;
96 match tbl {
97 Value::Nil => {
98 let tbl = lua.create_table()?;
99 tbl.set(1, call_stack)?;
100 lua.set_named_registry_value(&decorated_name, tbl)?;
101 Ok(())
102 }
103 Value::Table(tbl) => {
104 let len = tbl.raw_len();
105 tbl.set(len + 1, call_stack)?;
106 Ok(())
107 }
108 _ => Err(mlua::Error::external(format!(
109 "registry key for {decorated_name} has invalid type",
110 ))),
111 }
112 }
113
114 kumo_mod.set(
117 "get_event_registrars",
118 lua.create_function(move |lua, name: String| {
119 let decorated_name = event_registrar_name(&name);
120 let value: Value = lua.named_registry_value(&decorated_name)?;
121 Ok(value)
122 })?,
123 )?;
124
125 kumo_mod.set(
126 "on",
127 lua.create_function(move |lua, (name, func): (String, Function)| {
128 let decorated_name = decorate_callback_name(&name);
129
130 if let Ok(current_event) = lua.globals().get::<String>("_KUMO_CURRENT_EVENT") {
131 if current_event != "main" {
132 return Err(mlua::Error::external(format!(
133 "Attempting to register an event handler via \
134 `kumo.on('{name}', ...)` from within the event handler \
135 '{current_event}'. You must move your event handler registration \
136 so that it is setup directly when the policy is loaded \
137 in order for it to consistently trigger and handle events."
138 )));
139 }
140 }
141
142 register_event_caller(lua, &name)?;
143
144 if config::does_callback_allow_multiple(&name) {
145 let tbl: Value = lua.named_registry_value(&decorated_name)?;
146 return match tbl {
147 Value::Nil => {
148 let tbl = lua.create_table()?;
149 tbl.set(1, func)?;
150 lua.set_named_registry_value(&decorated_name, tbl)?;
151 Ok(())
152 }
153 Value::Table(tbl) => {
154 let len = tbl.raw_len();
155 tbl.set(len + 1, func)?;
156 Ok(())
157 }
158 _ => Err(mlua::Error::external(format!(
159 "registry key for {decorated_name} has invalid type",
160 ))),
161 };
162 }
163
164 let existing: Value = lua.named_registry_value(&decorated_name)?;
165 match existing {
166 Value::Nil => {}
167 Value::Function(func) => {
168 let info = func.info();
169 let src = info.source.unwrap_or_else(|| "?".into());
170 let line = info.line_defined.unwrap_or(0);
171 return Err(mlua::Error::external(format!(
172 "{name} event already has a handler defined at {src}:{line}"
173 )));
174 }
175 _ => {
176 return Err(mlua::Error::external(format!(
177 "{name} event already has a handler"
178 )));
179 }
180 }
181
182 lua.set_named_registry_value(&decorated_name, func)?;
183 Ok(())
184 })?,
185 )?;
186
187 kumo_mod.set(
188 "set_diagnostic_log_filter",
189 lua.create_function(move |_, filter: String| {
190 diagnostic_logging::set_diagnostic_log_filter(&filter).map_err(any_err)
191 })?,
192 )?;
193
194 fn variadic_to_string(args: Variadic<Value>) -> String {
195 let mut output = String::new();
196 for (idx, item) in args.into_iter().enumerate() {
197 if idx > 0 {
198 output.push(' ');
199 }
200
201 match item {
202 Value::String(s) => {
203 let bytes = s.as_bytes();
204 for (start, end, c) in bytes.char_indices() {
205 if c == std::char::REPLACEMENT_CHARACTER {
206 let c_slice = &bytes[start..end];
207 for &b in c_slice.iter() {
208 output.push_str(&format!("\\x{b:02X}"));
209 }
210 } else {
211 output.push(c);
212 }
213 }
214 }
215 item => match item.to_string() {
216 Ok(s) => output.push_str(&s),
217 Err(_) => output.push_str(&format!("{item:?}")),
218 },
219 }
220 }
221 output
222 }
223
224 fn get_caller(lua: &Lua) -> String {
225 match lua.inspect_stack(1, |info| {
226 let source = info.source();
227 let file_name = source
228 .short_src
229 .as_ref()
230 .map(|b| b.to_string())
231 .unwrap_or_else(String::new);
232 let file_name = match file_name.strip_prefix("[string \"") {
235 Some(name) => name.strip_suffix("\"]").unwrap_or(name),
236 None => &file_name,
237 };
238
239 format!("{file_name}:{}", info.current_line().unwrap_or(0))
240 }) {
241 Some(info) => info,
242 None => "?".to_string(),
243 }
244 }
245
246 kumo_mod.set(
247 "log_error",
248 lua.create_function(move |lua, args: Variadic<Value>| {
249 if tracing::event_enabled!(target: "lua", tracing::Level::ERROR) {
250 let src = get_caller(lua);
251 tracing::error!(target: "lua", "{src}: {}", variadic_to_string(args));
252 }
253 Ok(())
254 })?,
255 )?;
256 kumo_mod.set(
257 "log_info",
258 lua.create_function(move |lua, args: Variadic<Value>| {
259 if tracing::event_enabled!(target: "lua", tracing::Level::INFO) {
260 let src = get_caller(lua);
261 tracing::info!(target: "lua", "{src}: {}", variadic_to_string(args));
262 }
263 Ok(())
264 })?,
265 )?;
266 kumo_mod.set(
267 "log_warn",
268 lua.create_function(move |lua, args: Variadic<Value>| {
269 if tracing::event_enabled!(target: "lua", tracing::Level::WARN) {
270 let src = get_caller(lua);
271 tracing::warn!(target: "lua", "{src}: {}", variadic_to_string(args));
272 }
273 Ok(())
274 })?,
275 )?;
276 kumo_mod.set(
277 "log_debug",
278 lua.create_function(move |lua, args: Variadic<Value>| {
279 if tracing::event_enabled!(target: "lua", tracing::Level::DEBUG) {
280 let src = get_caller(lua);
281 tracing::debug!(target: "lua", "{src}: {}", variadic_to_string(args));
282 }
283 Ok(())
284 })?,
285 )?;
286
287 kumo_mod.set(
288 "set_max_spare_lua_contexts",
289 lua.create_function(move |_, limit: usize| {
290 config::set_max_spare(limit);
291 Ok(())
292 })?,
293 )?;
294
295 kumo_mod.set(
296 "set_max_lua_context_use_count",
297 lua.create_function(move |_, limit: usize| {
298 config::set_max_use(limit);
299 Ok(())
300 })?,
301 )?;
302
303 kumo_mod.set(
304 "set_max_lua_context_age",
305 lua.create_function(move |_, limit: usize| {
306 config::set_max_age(limit);
307 Ok(())
308 })?,
309 )?;
310
311 kumo_mod.set(
312 "set_lua_gc_on_put",
313 lua.create_function(move |_, enable: u8| {
314 config::set_gc_on_put(enable);
315 Ok(())
316 })?,
317 )?;
318
319 kumo_mod.set(
320 "set_lruttl_cache_capacity",
321 lua.create_function(move |_, (name, capacity): (String, usize)| {
322 if lruttl::set_cache_capacity(&name, capacity) {
323 Ok(())
324 } else {
325 Err(mlua::Error::external(format!(
326 "could not set capacity for cache {name} \
327 as that is not a pre-defined lruttl cache name"
328 )))
329 }
330 })?,
331 )?;
332
333 kumo_mod.set(
334 "set_config_monitor_globs",
335 lua.create_function(move |_, globs: Vec<String>| {
336 config::epoch::set_globs(globs).map_err(any_err)?;
337 Ok(())
338 })?,
339 )?;
340 kumo_mod.set(
341 "eval_config_monitor_globs",
342 lua.create_async_function(|_, _: ()| async move {
343 config::epoch::eval_globs().await.map_err(any_err)
344 })?,
345 )?;
346 kumo_mod.set(
347 "bump_config_epoch",
348 lua.create_function(move |_, _: ()| {
349 config::epoch::bump_current_epoch();
350 Ok(())
351 })?,
352 )?;
353
354 kumo_mod.set(
355 "available_parallelism",
356 lua.create_function(move |_, _: ()| available_parallelism().map_err(any_err))?,
357 )?;
358
359 kumo_mod.set(
360 "set_memory_hard_limit",
361 lua.create_function(move |_, limit: usize| {
362 kumo_server_memory::set_hard_limit(limit);
363 Ok(())
364 })?,
365 )?;
366
367 kumo_mod.set(
368 "set_memory_low_thresh",
369 lua.create_function(move |_, limit: usize| {
370 kumo_server_memory::set_low_memory_thresh(limit);
371 Ok(())
372 })?,
373 )?;
374
375 kumo_mod.set(
376 "set_memory_soft_limit",
377 lua.create_function(move |_, limit: usize| {
378 kumo_server_memory::set_soft_limit(limit);
379 Ok(())
380 })?,
381 )?;
382
383 kumo_mod.set(
384 "get_memory_hard_limit",
385 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_hard_limit()))?,
386 )?;
387
388 kumo_mod.set(
389 "get_memory_soft_limit",
390 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_soft_limit()))?,
391 )?;
392
393 kumo_mod.set(
394 "get_memory_low_thresh",
395 lua.create_function(move |_, _: ()| Ok(kumo_server_memory::get_low_memory_thresh()))?,
396 )?;
397
398 kumo_mod.set(
399 "configure_redis_throttles",
400 lua.create_async_function(|lua, params: Value| async move {
401 let key: RedisConnKey = from_lua_value(&lua, params)?;
402 let conn = key.open().map_err(any_err)?;
403 conn.ping().await.map_err(any_err)?;
404 throttle::use_redis(conn).await.map_err(any_err)
405 })?,
406 )?;
407
408 kumo_mod.set(
409 "traceback",
410 lua.create_function(move |lua: &Lua, level: usize| {
411 #[derive(Debug, Serialize)]
412 struct Frame {
413 event: String,
414 name: Option<String>,
415 name_what: Option<String>,
416 source: Option<String>,
417 short_src: Option<String>,
418 line_defined: Option<usize>,
419 last_line_defined: Option<usize>,
420 what: &'static str,
421 curr_line: Option<usize>,
422 is_tail_call: bool,
423 }
424
425 let mut frames = vec![];
426 for n in level.. {
427 match lua.inspect_stack(n, |info| {
428 let source = info.source();
429 let names = info.names();
430 Frame {
431 curr_line: info.current_line(),
432 is_tail_call: info.is_tail_call(),
433 event: format!("{:?}", info.event()),
434 last_line_defined: source.last_line_defined,
435 line_defined: source.line_defined,
436 name: names.name.as_ref().map(|b| b.to_string()),
437 name_what: names.name_what.as_ref().map(|b| b.to_string()),
438 source: source.source.as_ref().map(|b| b.to_string()),
439 short_src: source.short_src.as_ref().map(|b| b.to_string()),
440 what: source.what,
441 }
442 }) {
443 Some(frame) => {
444 frames.push(frame);
445 }
446 None => break,
447 }
448 }
449
450 lua.to_value(&frames)
451 })?,
452 )?;
453
454 #[derive(Deserialize, Debug)]
457 struct TaskParams {
458 event_name: String,
459 args: Vec<serde_json::Value>,
460 }
461
462 impl TaskParams {
463 async fn run(&self) -> anyhow::Result<()> {
464 let mut config = load_config().await?;
465
466 let sig = CallbackSignature::<Value, ()>::new(self.event_name.to_string());
467
468 config
469 .convert_args_and_call_callback(&sig, &self.args)
470 .await?;
471
472 config.put();
473
474 Ok(())
475 }
476 }
477
478 kumo_mod.set(
479 "spawn_task",
480 lua.create_function(|lua, params: Value| {
481 let params: TaskParams = lua.from_value(params)?;
482
483 if !config::is_validating() {
484 std::thread::Builder::new()
485 .name(format!("spawned-task-{}", params.event_name))
486 .spawn(move || {
487 let runtime = tokio::runtime::Builder::new_current_thread()
488 .enable_io()
489 .enable_time()
490 .on_thread_park(kumo_server_memory::purge_thread_cache)
491 .build()
492 .unwrap();
493 let event_name = params.event_name.clone();
494
495 let result = runtime.block_on(async move { params.run().await });
496 if let Err(err) = result {
497 tracing::error!("Error while dispatching {event_name}: {err:#}");
498 }
499 })?;
500 }
501
502 Ok(())
503 })?,
504 )?;
505
506 kumo_mod.set(
507 "spawn_thread_pool",
508 lua.create_function(|lua, params: Value| {
509 #[derive(Deserialize, Debug)]
510 struct ThreadPoolParams {
511 name: String,
512 num_threads: usize,
513 }
514
515 let params: ThreadPoolParams = lua.from_value(params)?;
516 let num_threads = AtomicUsize::new(params.num_threads);
517
518 if !config::is_validating() {
519 let _runtime = kumo_server_runtime::Runtime::new(
523 ¶ms.name,
524 |_| params.num_threads,
525 &num_threads,
526 )
527 .map_err(any_err)?;
528 }
529
530 Ok(())
531 })?,
532 )?;
533
534 kumo_mod.set(
535 "validation_failed",
536 lua.create_function(|_, ()| {
537 config::set_validation_failed();
538 Ok(())
539 })?,
540 )?;
541
542 kumo_mod.set(
543 "enable_memory_callstack_tracking",
544 lua.create_function(|_, enable: bool| {
545 kumo_server_memory::set_tracking_callstacks(enable);
546 Ok(())
547 })?,
548 )?;
549
550 kumo_mod.set(
554 "prometheus_metrics",
555 lua.create_async_function(|lua, ()| async move {
556 use tokio_stream::StreamExt;
557 let mut json_text = String::new();
558 let mut stream = kumo_prometheus::registry::Registry::stream_json();
559 while let Some(text) = stream.next().await {
560 json_text.push_str(&text);
561 }
562 let value: serde_json::Value = serde_json::from_str(&json_text).map_err(any_err)?;
563 lua.to_value_with(&value, serialize_options())
564 })?,
565 )?;
566
567 Ok(())
568}