1use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use chrono::{Local, SecondsFormat};
11use diaryx_core::fs::AsyncFileSystem;
12use diaryx_core::plugin::permissions::PermissionType;
13use extism::{CurrentPlugin, Error as ExtismError, UserData, Val, ValType};
14
15use crate::permission_checker::DenyAllPermissionChecker;
16
17pub trait PluginStorage: Send + Sync {
21 fn get(&self, key: &str) -> Option<Vec<u8>>;
23 fn set(&self, key: &str, data: &[u8]);
25 fn delete(&self, key: &str);
27}
28
29pub trait PluginSecretStore: Send + Sync {
31 fn get(&self, key: &str) -> Option<String>;
33 fn set(&self, key: &str, value: &str);
35 fn delete(&self, key: &str);
37}
38
39pub trait EventEmitter: Send + Sync {
41 fn emit(&self, event_json: &str);
43}
44
45pub trait WebSocketBridge: Send + Sync {
47 fn request(&self, request_json: &str) -> Result<String, String>;
49}
50
51pub trait PluginCommandBridge: Send + Sync {
53 fn call(
55 &self,
56 caller_plugin_id: &str,
57 plugin_id: &str,
58 command: &str,
59 params: serde_json::Value,
60 ) -> Result<serde_json::Value, String>;
61}
62
63pub trait RuntimeContextProvider: Send + Sync {
65 fn get_context(&self, plugin_id: &str) -> serde_json::Value;
67}
68
69#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
71pub struct NamespaceObjectMeta {
72 #[serde(default)]
73 pub namespace_id: Option<String>,
74 pub key: String,
75 #[serde(default)]
76 pub r2_key: Option<String>,
77 #[serde(default)]
78 pub audience: Option<String>,
79 #[serde(default)]
80 pub mime_type: Option<String>,
81 #[serde(default)]
82 pub size_bytes: Option<u64>,
83 #[serde(default)]
84 pub updated_at: Option<i64>,
85 #[serde(default)]
86 pub content_hash: Option<String>,
87}
88
89#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
91pub struct NamespaceEntry {
92 pub id: String,
93 pub owner_user_id: String,
94 pub created_at: i64,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub metadata: Option<serde_json::Value>,
97}
98
99#[derive(Debug, Clone)]
101pub struct BatchGetEntry {
102 pub bytes: Vec<u8>,
103 pub mime_type: String,
104}
105
106#[derive(Debug, Clone, Default)]
108pub struct BatchGetResult {
109 pub objects: std::collections::HashMap<String, BatchGetEntry>,
110 pub errors: std::collections::HashMap<String, String>,
111}
112
113pub trait NamespaceProvider: Send + Sync {
118 fn create_namespace(
119 &self,
120 metadata: Option<&serde_json::Value>,
121 ) -> Result<NamespaceEntry, String>;
122 fn put_object(
123 &self,
124 ns_id: &str,
125 key: &str,
126 bytes: &[u8],
127 mime_type: &str,
128 audience: Option<&str>,
129 ) -> Result<(), String>;
130 fn get_object(&self, ns_id: &str, key: &str) -> Result<Vec<u8>, String>;
131 fn delete_object(&self, ns_id: &str, key: &str) -> Result<(), String>;
132 fn list_objects(
133 &self,
134 ns_id: &str,
135 prefix: Option<&str>,
136 limit: Option<u32>,
137 offset: Option<u32>,
138 ) -> Result<Vec<NamespaceObjectMeta>, String>;
139 fn sync_audience(&self, ns_id: &str, audience: &str, access: &str) -> Result<(), String>;
140 fn send_audience_email(
142 &self,
143 ns_id: &str,
144 audience: &str,
145 subject: &str,
146 reply_to: Option<&str>,
147 ) -> Result<serde_json::Value, String>;
148
149 fn get_objects_batch(&self, ns_id: &str, keys: &[String]) -> Result<BatchGetResult, String>;
151
152 fn list_namespaces(&self) -> Result<Vec<NamespaceEntry>, String>;
154}
155
156pub fn parse_multipart_batch(body: &[u8], boundary: &str) -> BatchGetResult {
161 let mut result = BatchGetResult::default();
162 let delim = format!("--{boundary}");
163 let closing = format!("--{boundary}--");
164
165 let delim_bytes = delim.as_bytes();
167 let mut parts: Vec<&[u8]> = Vec::new();
168 let mut start = 0;
169
170 while let Some(pos) = memmem(body, start, delim_bytes) {
171 if start > 0 {
172 let end = if pos >= 2 && body[pos - 2] == b'\r' && body[pos - 1] == b'\n' {
174 pos - 2
175 } else {
176 pos
177 };
178 parts.push(&body[start..end]);
179 }
180 start = pos + delim_bytes.len();
181 if start < body.len() && body[start] == b'\r' {
183 start += 1;
184 }
185 if start < body.len() && body[start] == b'\n' {
186 start += 1;
187 }
188 if start >= 2 && body[start - 2..start].starts_with(b"--") {
190 break;
191 }
192 }
193
194 for part in parts {
195 let header_end = match memmem(part, 0, b"\r\n\r\n") {
197 Some(pos) => pos,
198 None => continue,
199 };
200 let header_section = &part[..header_end];
201 let body_section = &part[header_end + 4..];
202
203 let headers_str = String::from_utf8_lossy(header_section);
204 let mut filename: Option<String> = None;
205 let mut content_type = "application/octet-stream".to_string();
206 let mut is_error = false;
207
208 for line in headers_str.split("\r\n") {
209 let lower = line.to_ascii_lowercase();
210 if lower.starts_with("content-disposition:") {
211 if let Some(pos) = line.find("filename=\"") {
212 let start = pos + 10;
213 if let Some(end) = line[start..].find('\"') {
214 filename = Some(line[start..start + end].replace("\\\"", "\""));
215 }
216 }
217 } else if lower.starts_with("content-type:") {
218 content_type = line["content-type:".len()..].trim().to_string();
219 } else if lower.starts_with("x-batch-error:") {
220 is_error = lower.contains("true");
221 }
222 }
223
224 if let Some(key) = filename {
225 if is_error {
226 let msg = String::from_utf8_lossy(body_section).to_string();
227 result.errors.insert(key, msg);
228 } else {
229 result.objects.insert(
230 key,
231 BatchGetEntry {
232 bytes: body_section.to_vec(),
233 mime_type: content_type,
234 },
235 );
236 }
237 }
238 }
239
240 result
241}
242
243fn memmem(haystack: &[u8], offset: usize, needle: &[u8]) -> Option<usize> {
245 if needle.is_empty() || offset + needle.len() > haystack.len() {
246 return None;
247 }
248 haystack[offset..]
249 .windows(needle.len())
250 .position(|w| w == needle)
251 .map(|p| p + offset)
252}
253
254#[cfg(test)]
255mod multipart_tests {
256 use super::*;
257
258 fn build_multipart(boundary: &str, parts: &[(&str, &str, &[u8], bool)]) -> Vec<u8> {
259 let mut buf = Vec::new();
260 for (key, mime, body, is_error) in parts {
261 buf.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
262 buf.extend_from_slice(
263 format!("Content-Disposition: attachment; filename=\"{key}\"\r\n").as_bytes(),
264 );
265 if *is_error {
266 buf.extend_from_slice(b"X-Batch-Error: true\r\n");
267 }
268 buf.extend_from_slice(format!("Content-Type: {mime}\r\n").as_bytes());
269 buf.extend_from_slice(format!("Content-Length: {}\r\n", body.len()).as_bytes());
270 buf.extend_from_slice(b"\r\n");
271 buf.extend_from_slice(body);
272 buf.extend_from_slice(b"\r\n");
273 }
274 buf.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
275 buf
276 }
277
278 #[test]
279 fn parses_text_and_binary_parts() {
280 let boundary = "test-boundary-123";
281 let body = build_multipart(
282 boundary,
283 &[
284 ("files/readme.md", "text/markdown", b"# Hello", false),
285 (
286 "files/image.png",
287 "image/png",
288 &[0x89, 0x50, 0x4E, 0x47],
289 false,
290 ),
291 ],
292 );
293 let result = parse_multipart_batch(&body, boundary);
294 assert_eq!(result.objects.len(), 2);
295 assert!(result.errors.is_empty());
296
297 let md = result.objects.get("files/readme.md").unwrap();
298 assert_eq!(md.bytes, b"# Hello");
299 assert_eq!(md.mime_type, "text/markdown");
300
301 let img = result.objects.get("files/image.png").unwrap();
302 assert_eq!(img.bytes, &[0x89, 0x50, 0x4E, 0x47]);
303 assert_eq!(img.mime_type, "image/png");
304 }
305
306 #[test]
307 fn parses_error_parts() {
308 let boundary = "err-boundary";
309 let body = build_multipart(
310 boundary,
311 &[
312 ("files/ok.md", "text/markdown", b"content", false),
313 ("files/missing.md", "text/plain", b"Object not found", true),
314 ],
315 );
316 let result = parse_multipart_batch(&body, boundary);
317 assert_eq!(result.objects.len(), 1);
318 assert_eq!(result.errors.len(), 1);
319 assert_eq!(
320 result.errors.get("files/missing.md").unwrap(),
321 "Object not found"
322 );
323 }
324
325 #[test]
326 fn handles_empty_batch() {
327 let boundary = "empty";
328 let body = format!("--{boundary}--\r\n").into_bytes();
329 let result = parse_multipart_batch(&body, boundary);
330 assert!(result.objects.is_empty());
331 assert!(result.errors.is_empty());
332 }
333}
334
335pub struct NoopStorage;
337
338impl PluginStorage for NoopStorage {
339 fn get(&self, _key: &str) -> Option<Vec<u8>> {
340 None
341 }
342 fn set(&self, _key: &str, _data: &[u8]) {}
343 fn delete(&self, _key: &str) {}
344}
345
346pub struct NoopSecretStore;
348
349impl PluginSecretStore for NoopSecretStore {
350 fn get(&self, _key: &str) -> Option<String> {
351 None
352 }
353
354 fn set(&self, _key: &str, _value: &str) {}
355
356 fn delete(&self, _key: &str) {}
357}
358
359fn sanitize_storage_key(key: &str) -> String {
360 key.chars()
361 .map(|c| {
362 if c == '/' || c == '\\' || c == ':' {
363 '_'
364 } else {
365 c
366 }
367 })
368 .collect()
369}
370
371pub struct FilePluginStorage {
373 base_dir: PathBuf,
374}
375
376impl FilePluginStorage {
377 pub fn new(base_dir: PathBuf) -> Self {
378 let _ = std::fs::create_dir_all(&base_dir);
379 Self { base_dir }
380 }
381
382 fn key_to_path(&self, key: &str) -> PathBuf {
383 self.base_dir
384 .join(format!("{}.bin", sanitize_storage_key(key)))
385 }
386}
387
388impl PluginStorage for FilePluginStorage {
389 fn get(&self, key: &str) -> Option<Vec<u8>> {
390 std::fs::read(self.key_to_path(key)).ok()
391 }
392
393 fn set(&self, key: &str, data: &[u8]) {
394 let path = self.key_to_path(key);
395 if let Some(parent) = path.parent() {
396 let _ = std::fs::create_dir_all(parent);
397 }
398 let _ = std::fs::write(path, data);
399 }
400
401 fn delete(&self, key: &str) {
402 let _ = std::fs::remove_file(self.key_to_path(key));
403 }
404}
405
406pub struct FilePluginSecretStore {
408 base_dir: PathBuf,
409}
410
411impl FilePluginSecretStore {
412 pub fn new(base_dir: PathBuf) -> Self {
413 let _ = std::fs::create_dir_all(&base_dir);
414 Self { base_dir }
415 }
416
417 fn key_to_path(&self, key: &str) -> PathBuf {
418 self.base_dir
419 .join(format!("{}.secret", sanitize_storage_key(key)))
420 }
421}
422
423impl PluginSecretStore for FilePluginSecretStore {
424 fn get(&self, key: &str) -> Option<String> {
425 std::fs::read_to_string(self.key_to_path(key)).ok()
426 }
427
428 fn set(&self, key: &str, value: &str) {
429 let path = self.key_to_path(key);
430 if let Some(parent) = path.parent() {
431 let _ = std::fs::create_dir_all(parent);
432 }
433 let _ = std::fs::write(path, value);
434 }
435
436 fn delete(&self, key: &str) {
437 let _ = std::fs::remove_file(self.key_to_path(key));
438 }
439}
440
441pub trait FileProvider: Send + Sync {
447 fn get_file(&self, plugin_id: &str, key: &str) -> Option<Vec<u8>>;
449}
450
451pub struct NoopFileProvider;
453
454impl FileProvider for NoopFileProvider {
455 fn get_file(&self, _plugin_id: &str, _key: &str) -> Option<Vec<u8>> {
456 None
457 }
458}
459
460pub struct MapFileProvider {
464 files: std::collections::HashMap<String, Vec<u8>>,
465}
466
467impl MapFileProvider {
468 pub fn new(files: std::collections::HashMap<String, Vec<u8>>) -> Self {
469 Self { files }
470 }
471}
472
473impl FileProvider for MapFileProvider {
474 fn get_file(&self, _plugin_id: &str, key: &str) -> Option<Vec<u8>> {
475 self.files.get(key).cloned()
476 }
477}
478
479pub struct NoopEventEmitter;
481
482impl EventEmitter for NoopEventEmitter {
483 fn emit(&self, _event_json: &str) {}
484}
485
486pub struct NoopWebSocketBridge;
488
489impl WebSocketBridge for NoopWebSocketBridge {
490 fn request(&self, _request_json: &str) -> Result<String, String> {
491 Ok(String::new())
492 }
493}
494
495pub struct NoopPluginCommandBridge;
497
498impl PluginCommandBridge for NoopPluginCommandBridge {
499 fn call(
500 &self,
501 _caller_plugin_id: &str,
502 _plugin_id: &str,
503 _command: &str,
504 _params: serde_json::Value,
505 ) -> Result<serde_json::Value, String> {
506 Err("Plugin command bridge is not available".to_string())
507 }
508}
509
510pub struct NoopRuntimeContextProvider;
512
513impl RuntimeContextProvider for NoopRuntimeContextProvider {
514 fn get_context(&self, _plugin_id: &str) -> serde_json::Value {
515 serde_json::json!({})
516 }
517}
518
519pub struct NoopNamespaceProvider;
521
522impl NamespaceProvider for NoopNamespaceProvider {
523 fn create_namespace(
524 &self,
525 _metadata: Option<&serde_json::Value>,
526 ) -> Result<NamespaceEntry, String> {
527 Err("Namespace operations are not available".to_string())
528 }
529
530 fn put_object(
531 &self,
532 _ns_id: &str,
533 _key: &str,
534 _bytes: &[u8],
535 _mime_type: &str,
536 _audience: Option<&str>,
537 ) -> Result<(), String> {
538 Err("Namespace operations are not available".to_string())
539 }
540 fn get_object(&self, _ns_id: &str, _key: &str) -> Result<Vec<u8>, String> {
541 Err("Namespace operations are not available".to_string())
542 }
543 fn delete_object(&self, _ns_id: &str, _key: &str) -> Result<(), String> {
544 Err("Namespace operations are not available".to_string())
545 }
546 fn list_objects(
547 &self,
548 _ns_id: &str,
549 _prefix: Option<&str>,
550 _limit: Option<u32>,
551 _offset: Option<u32>,
552 ) -> Result<Vec<NamespaceObjectMeta>, String> {
553 Err("Namespace operations are not available".to_string())
554 }
555 fn sync_audience(&self, _ns_id: &str, _audience: &str, _access: &str) -> Result<(), String> {
556 Err("Namespace operations are not available".to_string())
557 }
558 fn send_audience_email(
559 &self,
560 _ns_id: &str,
561 _audience: &str,
562 _subject: &str,
563 _reply_to: Option<&str>,
564 ) -> Result<serde_json::Value, String> {
565 Err("Namespace operations are not available".to_string())
566 }
567
568 fn get_objects_batch(&self, _ns_id: &str, _keys: &[String]) -> Result<BatchGetResult, String> {
569 Err("Namespace operations are not available".to_string())
570 }
571
572 fn list_namespaces(&self) -> Result<Vec<NamespaceEntry>, String> {
573 Err("Namespace operations are not available".to_string())
574 }
575}
576
577pub trait PermissionChecker: Send + Sync {
582 fn check_permission(
587 &self,
588 plugin_id: &str,
589 permission_type: PermissionType,
590 target: &str,
591 ) -> Result<(), String>;
592}
593
594pub struct HostContext {
599 pub fs: Arc<dyn AsyncFileSystem>,
601 pub storage: Arc<dyn PluginStorage>,
603 pub secret_store: Arc<dyn PluginSecretStore>,
605 pub event_emitter: Arc<dyn EventEmitter>,
607 pub plugin_id: String,
609 pub plugin_id_locked: bool,
611 pub permission_checker: Option<Arc<dyn PermissionChecker>>,
613 pub file_provider: Arc<dyn FileProvider>,
615 pub ws_bridge: Arc<dyn WebSocketBridge>,
617 pub plugin_command_bridge: Arc<dyn PluginCommandBridge>,
619 pub runtime_context_provider: Arc<dyn RuntimeContextProvider>,
621 pub namespace_provider: Arc<dyn NamespaceProvider>,
623 pub plugin_command_depth: u32,
625 pub storage_quota_bytes: u64,
627}
628
629pub const DEFAULT_STORAGE_QUOTA_BYTES: u64 = 1024 * 1024;
631
632impl HostContext {
633 pub fn with_fs(fs: Arc<dyn AsyncFileSystem>) -> Self {
635 Self {
636 fs,
637 storage: Arc::new(NoopStorage),
638 secret_store: Arc::new(NoopSecretStore),
639 event_emitter: Arc::new(NoopEventEmitter),
640 plugin_id: String::new(),
641 plugin_id_locked: false,
642 permission_checker: Some(Arc::new(DenyAllPermissionChecker)),
643 file_provider: Arc::new(NoopFileProvider),
644 ws_bridge: Arc::new(NoopWebSocketBridge),
645 plugin_command_bridge: Arc::new(NoopPluginCommandBridge),
646 runtime_context_provider: Arc::new(NoopRuntimeContextProvider),
647 namespace_provider: Arc::new(NoopNamespaceProvider),
648 plugin_command_depth: 0,
649 storage_quota_bytes: DEFAULT_STORAGE_QUOTA_BYTES,
650 }
651 }
652
653 fn check_perm(&self, perm: PermissionType, target: &str) -> Result<(), ExtismError> {
655 if let Some(checker) = &self.permission_checker {
656 checker
657 .check_permission(&self.plugin_id, perm, target)
658 .map_err(|msg| ExtismError::msg(msg))
659 } else {
660 Err(ExtismError::msg(
661 "Permission checker is not configured for this plugin host context",
662 ))
663 }
664 }
665
666 fn validate_http_headers(
670 headers: &std::collections::HashMap<String, String>,
671 ) -> Result<(), ExtismError> {
672 for (name, value) in headers {
673 if name.contains('\n')
674 || name.contains('\r')
675 || name.contains('\0')
676 || value.contains('\n')
677 || value.contains('\r')
678 || value.contains('\0')
679 {
680 return Err(ExtismError::msg(format!(
681 "Invalid HTTP header: name or value contains forbidden characters (header: '{name}')"
682 )));
683 }
684 }
685 Ok(())
686 }
687
688 fn validate_file_path(path: &str) -> Result<String, ExtismError> {
693 let normalized = path.replace('\\', "/");
694 for component in normalized.split('/') {
695 if component == ".." {
696 return Err(ExtismError::msg(format!(
697 "Path traversal not allowed: '{path}'"
698 )));
699 }
700 }
701 Ok(path.to_string())
702 }
703
704 fn storage_key(&self, key: &str) -> String {
705 if self.plugin_id.is_empty() {
706 key.to_string()
707 } else {
708 format!("{}:{}", self.plugin_id, key)
709 }
710 }
711
712 fn secret_key(&self, key: &str) -> String {
713 self.storage_key(key)
714 }
715}
716
717unsafe impl Send for HostContext {}
720unsafe impl Sync for HostContext {}
721
722pub fn register_host_functions(
726 builder: extism::PluginBuilder<'_>,
727 user_data: UserData<HostContext>,
728) -> extism::PluginBuilder<'_> {
729 builder
730 .with_function(
731 "host_log",
732 [ValType::I64],
733 [ValType::I64],
734 user_data.clone(),
735 host_log,
736 )
737 .with_function(
738 "host_read_file",
739 [ValType::I64],
740 [ValType::I64],
741 user_data.clone(),
742 host_read_file,
743 )
744 .with_function(
745 "host_read_binary",
746 [ValType::I64],
747 [ValType::I64],
748 user_data.clone(),
749 host_read_binary,
750 )
751 .with_function(
752 "host_list_files",
753 [ValType::I64],
754 [ValType::I64],
755 user_data.clone(),
756 host_list_files,
757 )
758 .with_function(
759 "host_list_dir",
760 [ValType::I64],
761 [ValType::I64],
762 user_data.clone(),
763 host_list_dir,
764 )
765 .with_function(
766 "host_workspace_file_set",
767 [ValType::I64],
768 [ValType::I64],
769 user_data.clone(),
770 host_workspace_file_set,
771 )
772 .with_function(
773 "host_file_exists",
774 [ValType::I64],
775 [ValType::I64],
776 user_data.clone(),
777 host_file_exists,
778 )
779 .with_function(
780 "host_file_metadata",
781 [ValType::I64],
782 [ValType::I64],
783 user_data.clone(),
784 host_file_metadata,
785 )
786 .with_function(
787 "host_write_file",
788 [ValType::I64],
789 [ValType::I64],
790 user_data.clone(),
791 host_write_file,
792 )
793 .with_function(
794 "host_delete_file",
795 [ValType::I64],
796 [ValType::I64],
797 user_data.clone(),
798 host_delete_file,
799 )
800 .with_function(
801 "host_write_binary",
802 [ValType::I64],
803 [ValType::I64],
804 user_data.clone(),
805 host_write_binary,
806 )
807 .with_function(
808 "host_emit_event",
809 [ValType::I64],
810 [ValType::I64],
811 user_data.clone(),
812 host_emit_event,
813 )
814 .with_function(
815 "host_storage_get",
816 [ValType::I64],
817 [ValType::I64],
818 user_data.clone(),
819 host_storage_get,
820 )
821 .with_function(
822 "host_storage_set",
823 [ValType::I64],
824 [ValType::I64],
825 user_data.clone(),
826 host_storage_set,
827 )
828 .with_function(
829 "host_secret_get",
830 [ValType::I64],
831 [ValType::I64],
832 user_data.clone(),
833 host_secret_get,
834 )
835 .with_function(
836 "host_secret_set",
837 [ValType::I64],
838 [ValType::I64],
839 user_data.clone(),
840 host_secret_set,
841 )
842 .with_function(
843 "host_secret_delete",
844 [ValType::I64],
845 [ValType::I64],
846 user_data.clone(),
847 host_secret_delete,
848 )
849 .with_function(
850 "host_get_timestamp",
851 [ValType::I64],
852 [ValType::I64],
853 user_data.clone(),
854 host_get_timestamp,
855 )
856 .with_function(
857 "host_get_now",
858 [ValType::I64],
859 [ValType::I64],
860 user_data.clone(),
861 host_get_now,
862 )
863 .with_function(
864 "host_http_request",
865 [ValType::I64],
866 [ValType::I64],
867 user_data.clone(),
868 host_http_request,
869 )
870 .with_function(
871 "host_run_wasi_module",
872 [ValType::I64],
873 [ValType::I64],
874 user_data.clone(),
875 host_run_wasi_module,
876 )
877 .with_function(
878 "host_request_file",
879 [ValType::I64],
880 [ValType::I64],
881 user_data.clone(),
882 host_request_file,
883 )
884 .with_function(
885 "host_plugin_command",
886 [ValType::I64],
887 [ValType::I64],
888 user_data.clone(),
889 host_plugin_command,
890 )
891 .with_function(
892 "host_get_runtime_context",
893 [ValType::I64],
894 [ValType::I64],
895 user_data.clone(),
896 host_get_runtime_context,
897 )
898 .with_function(
899 "host_namespace_put_object",
900 [ValType::I64],
901 [ValType::I64],
902 user_data.clone(),
903 host_namespace_put_object,
904 )
905 .with_function(
906 "host_namespace_delete_object",
907 [ValType::I64],
908 [ValType::I64],
909 user_data.clone(),
910 host_namespace_delete_object,
911 )
912 .with_function(
913 "host_namespace_get_object",
914 [ValType::I64],
915 [ValType::I64],
916 user_data.clone(),
917 host_namespace_get_object,
918 )
919 .with_function(
920 "host_namespace_get_objects_batch",
921 [ValType::I64],
922 [ValType::I64],
923 user_data.clone(),
924 host_namespace_get_objects_batch,
925 )
926 .with_function(
927 "host_namespace_list_objects",
928 [ValType::I64],
929 [ValType::I64],
930 user_data.clone(),
931 host_namespace_list_objects,
932 )
933 .with_function(
934 "host_namespace_list",
935 [ValType::I64],
936 [ValType::I64],
937 user_data.clone(),
938 host_namespace_list,
939 )
940 .with_function(
941 "host_namespace_create",
942 [ValType::I64],
943 [ValType::I64],
944 user_data.clone(),
945 host_namespace_create,
946 )
947 .with_function(
948 "host_namespace_sync_audience",
949 [ValType::I64],
950 [ValType::I64],
951 user_data.clone(),
952 host_namespace_sync_audience,
953 )
954 .with_function(
955 "host_namespace_send_email",
956 [ValType::I64],
957 [ValType::I64],
958 user_data.clone(),
959 host_namespace_send_email,
960 )
961 .with_function(
962 "host_ws_request",
963 [ValType::I64],
964 [ValType::I64],
965 user_data.clone(),
966 host_ws_request,
967 )
968 .with_function(
969 "host_hash_file",
970 [ValType::I64],
971 [ValType::I64],
972 user_data.clone(),
973 host_hash_file,
974 )
975 .with_function(
976 "host_proxy_request",
977 [ValType::I64],
978 [ValType::I64],
979 user_data,
980 host_proxy_request,
981 )
982}
983
984fn host_log(
988 plugin: &mut CurrentPlugin,
989 inputs: &[Val],
990 outputs: &mut [Val],
991 _user_data: UserData<HostContext>,
992) -> Result<(), ExtismError> {
993 let input: String = plugin.memory_get_val(&inputs[0])?;
994
995 #[derive(serde::Deserialize)]
996 struct LogInput {
997 level: String,
998 message: String,
999 }
1000
1001 let parsed: LogInput = serde_json::from_str(&input)
1002 .map_err(|e| ExtismError::msg(format!("host_log: invalid input: {e}")))?;
1003
1004 match parsed.level.as_str() {
1005 "error" => log::error!("[extism-plugin] {}", parsed.message),
1006 "warn" => log::warn!("[extism-plugin] {}", parsed.message),
1007 "info" => log::info!("[extism-plugin] {}", parsed.message),
1008 "debug" => log::debug!("[extism-plugin] {}", parsed.message),
1009 _ => log::trace!("[extism-plugin] {}", parsed.message),
1010 }
1011
1012 plugin.memory_set_val(&mut outputs[0], "")?;
1013 Ok(())
1014}
1015
1016fn host_read_file(
1022 plugin: &mut CurrentPlugin,
1023 inputs: &[Val],
1024 outputs: &mut [Val],
1025 user_data: UserData<HostContext>,
1026) -> Result<(), ExtismError> {
1027 let input: String = plugin.memory_get_val(&inputs[0])?;
1028
1029 #[derive(serde::Deserialize)]
1030 struct ReadInput {
1031 path: String,
1032 }
1033
1034 let parsed: ReadInput = serde_json::from_str(&input)
1035 .map_err(|e| ExtismError::msg(format!("host_read_file: invalid input: {e}")))?;
1036 let path = HostContext::validate_file_path(&parsed.path)?;
1037
1038 let ctx = user_data.get()?;
1039 let ctx = ctx
1040 .lock()
1041 .map_err(|e| ExtismError::msg(format!("host_read_file: lock: {e}")))?;
1042
1043 if let Err(e) = ctx.check_perm(PermissionType::ReadFiles, &path) {
1044 let err = serde_json::json!({ "error": e.to_string() }).to_string();
1045 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1046 return Ok(());
1047 }
1048
1049 match futures_lite::future::block_on(ctx.fs.read_to_string(Path::new(&path))) {
1050 Ok(content) => {
1051 plugin.memory_set_val(&mut outputs[0], content.as_str())?;
1052 }
1053 Err(e) => {
1054 let err = serde_json::json!({ "error": format!("host_read_file: {e}") }).to_string();
1055 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1056 }
1057 }
1058 Ok(())
1059}
1060
1061fn host_read_binary(
1065 plugin: &mut CurrentPlugin,
1066 inputs: &[Val],
1067 outputs: &mut [Val],
1068 user_data: UserData<HostContext>,
1069) -> Result<(), ExtismError> {
1070 use base64::Engine;
1071
1072 let input: String = plugin.memory_get_val(&inputs[0])?;
1073
1074 #[derive(serde::Deserialize)]
1075 struct ReadInput {
1076 path: String,
1077 }
1078
1079 let parsed: ReadInput = serde_json::from_str(&input)
1080 .map_err(|e| ExtismError::msg(format!("host_read_binary: invalid input: {e}")))?;
1081 let path = HostContext::validate_file_path(&parsed.path)?;
1082
1083 let ctx = user_data.get()?;
1084 let ctx = ctx
1085 .lock()
1086 .map_err(|e| ExtismError::msg(format!("host_read_binary: lock: {e}")))?;
1087
1088 if let Err(e) = ctx.check_perm(PermissionType::ReadFiles, &path) {
1089 let err = serde_json::json!({ "error": e.to_string() }).to_string();
1090 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1091 return Ok(());
1092 }
1093
1094 match futures_lite::future::block_on(ctx.fs.read_binary(Path::new(&path))) {
1095 Ok(bytes) => {
1096 let json = serde_json::json!({
1097 "data": base64::engine::general_purpose::STANDARD.encode(&bytes)
1098 })
1099 .to_string();
1100 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1101 }
1102 Err(e) => {
1103 let err = serde_json::json!({ "error": format!("host_read_binary: {e}") }).to_string();
1104 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1105 }
1106 }
1107 Ok(())
1108}
1109
1110fn host_list_dir(
1116 plugin: &mut CurrentPlugin,
1117 inputs: &[Val],
1118 outputs: &mut [Val],
1119 user_data: UserData<HostContext>,
1120) -> Result<(), ExtismError> {
1121 let input: String = plugin.memory_get_val(&inputs[0])?;
1122
1123 #[derive(serde::Deserialize)]
1124 struct ListDirInput {
1125 path: String,
1126 }
1127
1128 let parsed: ListDirInput = serde_json::from_str(&input)
1129 .map_err(|e| ExtismError::msg(format!("host_list_dir: invalid input: {e}")))?;
1130 let dir_path = HostContext::validate_file_path(&parsed.path)?;
1131
1132 let ctx = user_data.get()?;
1133 let ctx = ctx
1134 .lock()
1135 .map_err(|e| ExtismError::msg(format!("host_list_dir: lock: {e}")))?;
1136 if let Err(e) = ctx.check_perm(PermissionType::ReadFiles, &dir_path) {
1137 let err = serde_json::json!({ "error": e.to_string() }).to_string();
1138 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1139 return Ok(());
1140 }
1141 let files = match futures_lite::future::block_on(ctx.fs.list_files(Path::new(&dir_path))) {
1142 Ok(files) => files,
1143 Err(e) => {
1144 let err = serde_json::json!({ "error": format!("host_list_dir: {e}") }).to_string();
1145 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1146 return Ok(());
1147 }
1148 };
1149
1150 let file_strings: Vec<String> = files
1151 .iter()
1152 .map(|p| p.to_string_lossy().to_string())
1153 .collect();
1154 let json = serde_json::to_string(&file_strings)
1155 .map_err(|e| ExtismError::msg(format!("host_list_dir: serialize: {e}")))?;
1156
1157 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1158 Ok(())
1159}
1160
1161fn host_list_files(
1165 plugin: &mut CurrentPlugin,
1166 inputs: &[Val],
1167 outputs: &mut [Val],
1168 user_data: UserData<HostContext>,
1169) -> Result<(), ExtismError> {
1170 let input: String = plugin.memory_get_val(&inputs[0])?;
1171
1172 #[derive(serde::Deserialize)]
1173 struct ListInput {
1174 prefix: String,
1175 }
1176
1177 let parsed: ListInput = serde_json::from_str(&input)
1178 .map_err(|e| ExtismError::msg(format!("host_list_files: invalid input: {e}")))?;
1179 let prefix = HostContext::validate_file_path(&parsed.prefix)?;
1180
1181 let ctx = user_data.get()?;
1182 let ctx = ctx
1183 .lock()
1184 .map_err(|e| ExtismError::msg(format!("host_list_files: lock: {e}")))?;
1185 if let Err(e) = ctx.check_perm(PermissionType::ReadFiles, &prefix) {
1186 let err = serde_json::json!({ "error": e.to_string() }).to_string();
1187 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1188 return Ok(());
1189 }
1190 let files =
1191 match futures_lite::future::block_on(ctx.fs.list_all_files_recursive(Path::new(&prefix))) {
1192 Ok(files) => files,
1193 Err(e) => {
1194 let err =
1195 serde_json::json!({ "error": format!("host_list_files: {e}") }).to_string();
1196 plugin.memory_set_val(&mut outputs[0], err.as_str())?;
1197 return Ok(());
1198 }
1199 };
1200
1201 let file_strings: Vec<String> = files
1202 .iter()
1203 .map(|p| p.to_string_lossy().to_string())
1204 .collect();
1205 let json = serde_json::to_string(&file_strings)
1206 .map_err(|e| ExtismError::msg(format!("host_list_files: serialize: {e}")))?;
1207
1208 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1209 Ok(())
1210}
1211
1212fn host_workspace_file_set(
1217 plugin: &mut CurrentPlugin,
1218 _inputs: &[Val],
1219 outputs: &mut [Val],
1220 user_data: UserData<HostContext>,
1221) -> Result<(), ExtismError> {
1222 fn inner(user_data: &UserData<HostContext>) -> Result<Vec<String>, String> {
1226 let ctx = user_data
1227 .get()
1228 .map_err(|e| format!("host_workspace_file_set: user_data: {e}"))?;
1229 let ctx = ctx
1230 .lock()
1231 .map_err(|e| format!("host_workspace_file_set: lock: {e}"))?;
1232 let runtime = ctx.runtime_context_provider.get_context(&ctx.plugin_id);
1233 let workspace_path = runtime
1234 .get("current_workspace")
1235 .and_then(|value| value.as_object())
1236 .and_then(|workspace| workspace.get("path"))
1237 .and_then(|value| value.as_str())
1238 .filter(|value| !value.trim().is_empty())
1239 .ok_or("host_workspace_file_set: missing current_workspace.path")?;
1240
1241 ctx.check_perm(PermissionType::ReadFiles, workspace_path)
1242 .map_err(|e| e.to_string())?;
1243
1244 let workspace = diaryx_core::workspace::Workspace::new(ctx.fs.as_ref());
1245 let workspace_path = Path::new(workspace_path);
1246 let root_index = if workspace_path
1247 .extension()
1248 .is_some_and(|extension| extension == "md")
1249 {
1250 workspace_path.to_path_buf()
1251 } else {
1252 futures_lite::future::block_on(workspace.find_root_index_in_dir(workspace_path))
1253 .map_err(|e| format!("host_workspace_file_set: {e}"))?
1254 .ok_or("host_workspace_file_set: workspace root index not found")?
1255 };
1256
1257 futures_lite::future::block_on(workspace.collect_workspace_file_set(&root_index))
1258 .map_err(|e| format!("host_workspace_file_set: {e}"))
1259 }
1260
1261 match inner(&user_data) {
1262 Ok(files) => {
1263 let json = serde_json::to_string(&files).map_err(|e| {
1264 ExtismError::msg(format!("host_workspace_file_set: serialize: {e}"))
1265 })?;
1266 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1267 }
1268 Err(msg) => {
1269 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1272 }
1273 }
1274 Ok(())
1275}
1276
1277fn host_file_exists(
1281 plugin: &mut CurrentPlugin,
1282 inputs: &[Val],
1283 outputs: &mut [Val],
1284 user_data: UserData<HostContext>,
1285) -> Result<(), ExtismError> {
1286 let input: String = plugin.memory_get_val(&inputs[0])?;
1287
1288 #[derive(serde::Deserialize)]
1289 struct ExistsInput {
1290 path: String,
1291 }
1292
1293 let parsed: ExistsInput = serde_json::from_str(&input)
1294 .map_err(|e| ExtismError::msg(format!("host_file_exists: invalid input: {e}")))?;
1295 let path = HostContext::validate_file_path(&parsed.path)?;
1296
1297 let ctx = user_data.get()?;
1298 let ctx = ctx
1299 .lock()
1300 .map_err(|e| ExtismError::msg(format!("host_file_exists: lock: {e}")))?;
1301 if ctx.check_perm(PermissionType::ReadFiles, &path).is_err() {
1304 plugin.memory_set_val(&mut outputs[0], "false")?;
1305 return Ok(());
1306 }
1307 let exists = futures_lite::future::block_on(ctx.fs.exists(Path::new(&path)));
1308
1309 let json = serde_json::to_string(&exists)
1310 .map_err(|e| ExtismError::msg(format!("host_file_exists: serialize: {e}")))?;
1311
1312 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1313 Ok(())
1314}
1315
1316fn host_file_metadata(
1320 plugin: &mut CurrentPlugin,
1321 inputs: &[Val],
1322 outputs: &mut [Val],
1323 user_data: UserData<HostContext>,
1324) -> Result<(), ExtismError> {
1325 let input: String = plugin.memory_get_val(&inputs[0])?;
1326
1327 #[derive(serde::Deserialize)]
1328 struct MetadataInput {
1329 path: String,
1330 }
1331
1332 let parsed: MetadataInput = serde_json::from_str(&input)
1333 .map_err(|e| ExtismError::msg(format!("host_file_metadata: invalid input: {e}")))?;
1334 let validated_path = HostContext::validate_file_path(&parsed.path)?;
1335
1336 let not_found = serde_json::json!({
1337 "exists": false,
1338 "size_bytes": serde_json::Value::Null,
1339 "modified_at_ms": serde_json::Value::Null,
1340 })
1341 .to_string();
1342
1343 let ctx = user_data.get()?;
1344 let ctx = ctx
1345 .lock()
1346 .map_err(|e| ExtismError::msg(format!("host_file_metadata: lock: {e}")))?;
1347 if ctx
1349 .check_perm(PermissionType::ReadFiles, &validated_path)
1350 .is_err()
1351 {
1352 plugin.memory_set_val(&mut outputs[0], not_found.as_str())?;
1353 return Ok(());
1354 }
1355 let path = Path::new(&validated_path);
1356 let exists = futures_lite::future::block_on(ctx.fs.exists(path));
1357 let json = if exists {
1358 let size_bytes = futures_lite::future::block_on(ctx.fs.get_file_size(path));
1359 let modified_at_ms = futures_lite::future::block_on(ctx.fs.get_modified_time(path));
1360 serde_json::json!({
1361 "exists": true,
1362 "size_bytes": size_bytes,
1363 "modified_at_ms": modified_at_ms,
1364 })
1365 .to_string()
1366 } else {
1367 not_found
1368 };
1369
1370 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1371 Ok(())
1372}
1373
1374fn host_write_file(
1378 plugin: &mut CurrentPlugin,
1379 inputs: &[Val],
1380 outputs: &mut [Val],
1381 user_data: UserData<HostContext>,
1382) -> Result<(), ExtismError> {
1383 let input: String = plugin.memory_get_val(&inputs[0])?;
1384
1385 #[derive(serde::Deserialize)]
1386 struct WriteInput {
1387 path: String,
1388 content: String,
1389 }
1390
1391 let parsed: WriteInput = serde_json::from_str(&input)
1392 .map_err(|e| ExtismError::msg(format!("host_write_file: invalid input: {e}")))?;
1393 let path = HostContext::validate_file_path(&parsed.path)?;
1394
1395 let ctx = user_data.get()?;
1396 let ctx = ctx
1397 .lock()
1398 .map_err(|e| ExtismError::msg(format!("host_write_file: lock: {e}")))?;
1399 let exists = futures_lite::future::block_on(ctx.fs.exists(Path::new(&path)));
1400 let perm = if exists {
1401 PermissionType::EditFiles
1402 } else {
1403 PermissionType::CreateFiles
1404 };
1405 if let Err(e) = ctx.check_perm(perm, &path) {
1406 plugin.memory_set_val(&mut outputs[0], e.to_string().as_str())?;
1407 return Ok(());
1408 }
1409 if let Err(e) =
1415 futures_lite::future::block_on(ctx.fs.write_file(Path::new(&path), &parsed.content))
1416 {
1417 let msg = format!("host_write_file: {e}");
1418 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1419 return Ok(());
1420 }
1421
1422 plugin.memory_set_val(&mut outputs[0], "")?;
1423 Ok(())
1424}
1425
1426fn host_delete_file(
1431 plugin: &mut CurrentPlugin,
1432 inputs: &[Val],
1433 outputs: &mut [Val],
1434 user_data: UserData<HostContext>,
1435) -> Result<(), ExtismError> {
1436 let input: String = plugin.memory_get_val(&inputs[0])?;
1437
1438 #[derive(serde::Deserialize)]
1439 struct DeleteInput {
1440 path: String,
1441 }
1442
1443 let parsed: DeleteInput = serde_json::from_str(&input)
1444 .map_err(|e| ExtismError::msg(format!("host_delete_file: invalid input: {e}")))?;
1445 let path = HostContext::validate_file_path(&parsed.path)?;
1446
1447 let ctx = user_data.get()?;
1448 let ctx = ctx
1449 .lock()
1450 .map_err(|e| ExtismError::msg(format!("host_delete_file: lock: {e}")))?;
1451 if let Err(e) = ctx.check_perm(PermissionType::DeleteFiles, &path) {
1452 plugin.memory_set_val(&mut outputs[0], e.to_string().as_str())?;
1453 return Ok(());
1454 }
1455 if let Err(e) = futures_lite::future::block_on(ctx.fs.delete_file(Path::new(&path))) {
1458 let msg = format!("host_delete_file: {e}");
1459 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1460 return Ok(());
1461 }
1462
1463 plugin.memory_set_val(&mut outputs[0], "")?;
1464 Ok(())
1465}
1466
1467fn host_write_binary(
1472 plugin: &mut CurrentPlugin,
1473 inputs: &[Val],
1474 outputs: &mut [Val],
1475 user_data: UserData<HostContext>,
1476) -> Result<(), ExtismError> {
1477 use base64::Engine;
1478
1479 let input: String = plugin.memory_get_val(&inputs[0])?;
1480
1481 #[derive(serde::Deserialize)]
1482 struct WriteBinaryInput {
1483 path: String,
1484 content: String, }
1486
1487 let parsed: WriteBinaryInput = serde_json::from_str(&input)
1488 .map_err(|e| ExtismError::msg(format!("host_write_binary: invalid input: {e}")))?;
1489
1490 let bytes = base64::engine::general_purpose::STANDARD
1491 .decode(&parsed.content)
1492 .map_err(|e| ExtismError::msg(format!("host_write_binary: base64 decode: {e}")))?;
1493
1494 let path = HostContext::validate_file_path(&parsed.path)?;
1495
1496 let ctx = user_data.get()?;
1497 let ctx = ctx
1498 .lock()
1499 .map_err(|e| ExtismError::msg(format!("host_write_binary: lock: {e}")))?;
1500 let exists = futures_lite::future::block_on(ctx.fs.exists(Path::new(&path)));
1501 let perm = if exists {
1502 PermissionType::EditFiles
1503 } else {
1504 PermissionType::CreateFiles
1505 };
1506 if let Err(e) = ctx.check_perm(perm, &path) {
1507 plugin.memory_set_val(&mut outputs[0], e.to_string().as_str())?;
1508 return Ok(());
1509 }
1510 if let Err(e) = futures_lite::future::block_on(ctx.fs.write_binary(Path::new(&path), &bytes)) {
1513 let msg = format!("host_write_binary: {e}");
1514 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1515 return Ok(());
1516 }
1517
1518 plugin.memory_set_val(&mut outputs[0], "")?;
1519 Ok(())
1520}
1521
1522fn host_emit_event(
1526 plugin: &mut CurrentPlugin,
1527 inputs: &[Val],
1528 outputs: &mut [Val],
1529 user_data: UserData<HostContext>,
1530) -> Result<(), ExtismError> {
1531 let event_json: String = plugin.memory_get_val(&inputs[0])?;
1532
1533 let ctx = user_data.get()?;
1534 let ctx = ctx
1535 .lock()
1536 .map_err(|e| ExtismError::msg(format!("host_emit_event: lock: {e}")))?;
1537 ctx.event_emitter.emit(&event_json);
1538
1539 plugin.memory_set_val(&mut outputs[0], "")?;
1540 Ok(())
1541}
1542
1543fn host_storage_get(
1547 plugin: &mut CurrentPlugin,
1548 inputs: &[Val],
1549 outputs: &mut [Val],
1550 user_data: UserData<HostContext>,
1551) -> Result<(), ExtismError> {
1552 use base64::Engine;
1553
1554 let input: String = plugin.memory_get_val(&inputs[0])?;
1555
1556 #[derive(serde::Deserialize)]
1557 struct StorageGetInput {
1558 key: String,
1559 }
1560
1561 let parsed: StorageGetInput = serde_json::from_str(&input)
1562 .map_err(|e| ExtismError::msg(format!("host_storage_get: invalid input: {e}")))?;
1563
1564 let ctx = user_data.get()?;
1565 let ctx = ctx
1566 .lock()
1567 .map_err(|e| ExtismError::msg(format!("host_storage_get: lock: {e}")))?;
1568 if ctx
1570 .check_perm(PermissionType::PluginStorage, &parsed.key)
1571 .is_err()
1572 {
1573 plugin.memory_set_val(&mut outputs[0], "")?;
1574 return Ok(());
1575 }
1576 let storage_key = ctx.storage_key(&parsed.key);
1577
1578 let result = match ctx.storage.get(&storage_key) {
1579 Some(data) => {
1580 let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
1581 serde_json::json!({ "data": encoded }).to_string()
1582 }
1583 None => String::new(),
1584 };
1585
1586 plugin.memory_set_val(&mut outputs[0], result.as_str())?;
1587 Ok(())
1588}
1589
1590fn host_storage_set(
1594 plugin: &mut CurrentPlugin,
1595 inputs: &[Val],
1596 outputs: &mut [Val],
1597 user_data: UserData<HostContext>,
1598) -> Result<(), ExtismError> {
1599 use base64::Engine;
1600
1601 let input: String = plugin.memory_get_val(&inputs[0])?;
1602
1603 #[derive(serde::Deserialize)]
1604 struct StorageSetInput {
1605 key: String,
1606 data: String, }
1608
1609 let parsed: StorageSetInput = serde_json::from_str(&input)
1610 .map_err(|e| ExtismError::msg(format!("host_storage_set: invalid input: {e}")))?;
1611
1612 let bytes = base64::engine::general_purpose::STANDARD
1613 .decode(&parsed.data)
1614 .map_err(|e| ExtismError::msg(format!("host_storage_set: base64 decode: {e}")))?;
1615
1616 let ctx = user_data.get()?;
1617 let ctx = ctx
1618 .lock()
1619 .map_err(|e| ExtismError::msg(format!("host_storage_set: lock: {e}")))?;
1620 if let Err(e) = ctx.check_perm(PermissionType::PluginStorage, &parsed.key) {
1622 let msg = format!("host_storage_set: {e}");
1623 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1624 return Ok(());
1625 }
1626 if ctx.storage_quota_bytes > 0 && bytes.len() as u64 > ctx.storage_quota_bytes {
1627 let msg = format!(
1628 "host_storage_set: data size ({} bytes) exceeds plugin storage quota ({} bytes)",
1629 bytes.len(),
1630 ctx.storage_quota_bytes
1631 );
1632 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1633 return Ok(());
1634 }
1635 let storage_key = ctx.storage_key(&parsed.key);
1636 ctx.storage.set(&storage_key, &bytes);
1637
1638 plugin.memory_set_val(&mut outputs[0], "")?;
1639 Ok(())
1640}
1641
1642fn host_secret_get(
1646 plugin: &mut CurrentPlugin,
1647 inputs: &[Val],
1648 outputs: &mut [Val],
1649 user_data: UserData<HostContext>,
1650) -> Result<(), ExtismError> {
1651 let input: String = plugin.memory_get_val(&inputs[0])?;
1652
1653 #[derive(serde::Deserialize)]
1654 struct SecretGetInput {
1655 key: String,
1656 }
1657
1658 let parsed: SecretGetInput = serde_json::from_str(&input)
1659 .map_err(|e| ExtismError::msg(format!("host_secret_get: invalid input: {e}")))?;
1660
1661 let ctx = user_data.get()?;
1662 let ctx = ctx
1663 .lock()
1664 .map_err(|e| ExtismError::msg(format!("host_secret_get: lock: {e}")))?;
1665 if ctx
1667 .check_perm(PermissionType::PluginStorage, &parsed.key)
1668 .is_err()
1669 {
1670 plugin.memory_set_val(&mut outputs[0], "")?;
1671 return Ok(());
1672 }
1673 let secret_key = ctx.secret_key(&parsed.key);
1674
1675 let result = match ctx.secret_store.get(&secret_key) {
1676 Some(value) => serde_json::json!({ "value": value }).to_string(),
1677 None => String::new(),
1678 };
1679
1680 plugin.memory_set_val(&mut outputs[0], result.as_str())?;
1681 Ok(())
1682}
1683
1684fn host_secret_set(
1688 plugin: &mut CurrentPlugin,
1689 inputs: &[Val],
1690 outputs: &mut [Val],
1691 user_data: UserData<HostContext>,
1692) -> Result<(), ExtismError> {
1693 let input: String = plugin.memory_get_val(&inputs[0])?;
1694
1695 #[derive(serde::Deserialize)]
1696 struct SecretSetInput {
1697 key: String,
1698 value: String,
1699 }
1700
1701 let parsed: SecretSetInput = serde_json::from_str(&input)
1702 .map_err(|e| ExtismError::msg(format!("host_secret_set: invalid input: {e}")))?;
1703
1704 let ctx = user_data.get()?;
1705 let ctx = ctx
1706 .lock()
1707 .map_err(|e| ExtismError::msg(format!("host_secret_set: lock: {e}")))?;
1708 if let Err(e) = ctx.check_perm(PermissionType::PluginStorage, &parsed.key) {
1710 let msg = format!("host_secret_set: {e}");
1711 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1712 return Ok(());
1713 }
1714 let secret_key = ctx.secret_key(&parsed.key);
1715 ctx.secret_store.set(&secret_key, &parsed.value);
1716
1717 plugin.memory_set_val(&mut outputs[0], "")?;
1718 Ok(())
1719}
1720
1721fn host_secret_delete(
1725 plugin: &mut CurrentPlugin,
1726 inputs: &[Val],
1727 outputs: &mut [Val],
1728 user_data: UserData<HostContext>,
1729) -> Result<(), ExtismError> {
1730 let input: String = plugin.memory_get_val(&inputs[0])?;
1731
1732 #[derive(serde::Deserialize)]
1733 struct SecretDeleteInput {
1734 key: String,
1735 }
1736
1737 let parsed: SecretDeleteInput = serde_json::from_str(&input)
1738 .map_err(|e| ExtismError::msg(format!("host_secret_delete: invalid input: {e}")))?;
1739
1740 let ctx = user_data.get()?;
1741 let ctx = ctx
1742 .lock()
1743 .map_err(|e| ExtismError::msg(format!("host_secret_delete: lock: {e}")))?;
1744 if let Err(e) = ctx.check_perm(PermissionType::PluginStorage, &parsed.key) {
1746 let msg = format!("host_secret_delete: {e}");
1747 plugin.memory_set_val(&mut outputs[0], msg.as_str())?;
1748 return Ok(());
1749 }
1750 let secret_key = ctx.secret_key(&parsed.key);
1751 ctx.secret_store.delete(&secret_key);
1752
1753 plugin.memory_set_val(&mut outputs[0], "")?;
1754 Ok(())
1755}
1756
1757#[cfg(feature = "wasi-runner")]
1764fn host_run_wasi_module(
1765 plugin: &mut CurrentPlugin,
1766 inputs: &[Val],
1767 outputs: &mut [Val],
1768 user_data: UserData<HostContext>,
1769) -> Result<(), ExtismError> {
1770 use base64::Engine;
1771
1772 fn err_envelope(msg: &str) -> String {
1775 serde_json::json!({
1776 "exit_code": -1,
1777 "stdout": "",
1778 "stderr": msg,
1779 "files": serde_json::Value::Null,
1780 "error": msg,
1781 })
1782 .to_string()
1783 }
1784
1785 let input: String = plugin.memory_get_val(&inputs[0])?;
1786 let request: crate::wasi_runner::WasiRunRequest = match serde_json::from_str(&input) {
1787 Ok(req) => req,
1788 Err(e) => {
1789 let msg = format!("host_run_wasi_module: invalid input: {e}");
1790 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1791 return Ok(());
1792 }
1793 };
1794
1795 let ctx = user_data.get()?;
1797 let ctx = ctx
1798 .lock()
1799 .map_err(|e| ExtismError::msg(format!("host_run_wasi_module: lock: {e}")))?;
1800 if let Err(e) = ctx.check_perm(PermissionType::PluginStorage, &request.module_key) {
1801 let msg = format!("host_run_wasi_module: {e}");
1802 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1803 return Ok(());
1804 }
1805 let storage_key = ctx.storage_key(&request.module_key);
1806 let wasm_bytes = match ctx.storage.get(&storage_key) {
1807 Some(bytes) => bytes,
1808 None => {
1809 let msg = format!(
1810 "host_run_wasi_module: module not found in storage: {}",
1811 request.module_key
1812 );
1813 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1814 return Ok(());
1815 }
1816 };
1817 drop(ctx);
1818
1819 let decoded_files = if let Some(ref files) = request.files {
1821 let mut map = std::collections::HashMap::new();
1822 for (path, b64) in files {
1823 match base64::engine::general_purpose::STANDARD.decode(b64) {
1824 Ok(data) => {
1825 map.insert(path.clone(), data);
1826 }
1827 Err(e) => {
1828 let msg = format!("host_run_wasi_module: base64 decode for {path}: {e}");
1829 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1830 return Ok(());
1831 }
1832 }
1833 }
1834 Some(map)
1835 } else {
1836 None
1837 };
1838
1839 let stdin_bytes = if let Some(ref b64) = request.stdin {
1841 match base64::engine::general_purpose::STANDARD.decode(b64) {
1842 Ok(bytes) => Some(bytes),
1843 Err(e) => {
1844 let msg = format!("host_run_wasi_module: stdin base64 decode: {e}");
1845 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1846 return Ok(());
1847 }
1848 }
1849 } else {
1850 None
1851 };
1852
1853 let result = match crate::wasi_runner::run_wasi_module(
1855 &wasm_bytes,
1856 &request.args,
1857 stdin_bytes.as_deref(),
1858 decoded_files.as_ref(),
1859 request.output_files.as_deref(),
1860 ) {
1861 Ok(result) => result,
1862 Err(e) => {
1863 let msg = format!("host_run_wasi_module: {e}");
1864 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
1865 return Ok(());
1866 }
1867 };
1868
1869 let json = serde_json::to_string(&result)
1870 .map_err(|e| ExtismError::msg(format!("host_run_wasi_module: serialize: {e}")))?;
1871
1872 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
1873 Ok(())
1874}
1875
1876#[cfg(not(feature = "wasi-runner"))]
1878fn host_run_wasi_module(
1879 plugin: &mut CurrentPlugin,
1880 _inputs: &[Val],
1881 outputs: &mut [Val],
1882 _user_data: UserData<HostContext>,
1883) -> Result<(), ExtismError> {
1884 let error = serde_json::json!({
1885 "exit_code": -1,
1886 "stdout": "",
1887 "stderr": "host_run_wasi_module: wasi-runner feature not enabled"
1888 });
1889 plugin.memory_set_val(&mut outputs[0], error.to_string().as_str())?;
1890 Ok(())
1891}
1892
1893fn host_get_timestamp(
1897 plugin: &mut CurrentPlugin,
1898 _inputs: &[Val],
1899 outputs: &mut [Val],
1900 _user_data: UserData<HostContext>,
1901) -> Result<(), ExtismError> {
1902 let now = std::time::SystemTime::now()
1903 .duration_since(std::time::UNIX_EPOCH)
1904 .map(|d| d.as_millis() as u64)
1905 .unwrap_or(0);
1906
1907 plugin.memory_set_val(&mut outputs[0], now.to_string().as_str())?;
1908 Ok(())
1909}
1910
1911fn host_get_now(
1913 plugin: &mut CurrentPlugin,
1914 _inputs: &[Val],
1915 outputs: &mut [Val],
1916 _user_data: UserData<HostContext>,
1917) -> Result<(), ExtismError> {
1918 let now = Local::now().to_rfc3339_opts(SecondsFormat::Secs, false);
1919 plugin.memory_set_val(&mut outputs[0], now.as_str())?;
1920 Ok(())
1921}
1922
1923fn host_request_file(
1929 plugin: &mut CurrentPlugin,
1930 inputs: &[Val],
1931 outputs: &mut [Val],
1932 user_data: UserData<HostContext>,
1933) -> Result<(), ExtismError> {
1934 let input: String = plugin.memory_get_val(&inputs[0])?;
1935
1936 #[derive(serde::Deserialize)]
1937 struct RequestFileInput {
1938 key: String,
1939 }
1940
1941 let parsed: RequestFileInput = serde_json::from_str(&input)
1942 .map_err(|e| ExtismError::msg(format!("host_request_file: invalid input: {e}")))?;
1943
1944 let ctx = user_data.get()?;
1945 let ctx = ctx
1946 .lock()
1947 .map_err(|e| ExtismError::msg(format!("host_request_file: lock: {e}")))?;
1948
1949 let result = ctx
1950 .file_provider
1951 .get_file(&ctx.plugin_id, &parsed.key)
1952 .unwrap_or_default();
1953
1954 plugin.memory_set_val(&mut outputs[0], result.as_slice())?;
1955 Ok(())
1956}
1957
1958fn host_plugin_command(
1962 plugin: &mut CurrentPlugin,
1963 inputs: &[Val],
1964 outputs: &mut [Val],
1965 user_data: UserData<HostContext>,
1966) -> Result<(), ExtismError> {
1967 #[derive(serde::Deserialize)]
1968 struct PluginCommandInput {
1969 plugin_id: String,
1970 command: String,
1971 #[serde(default)]
1972 params: serde_json::Value,
1973 }
1974
1975 let input: String = plugin.memory_get_val(&inputs[0])?;
1976 let parsed: PluginCommandInput = serde_json::from_str(&input)
1977 .map_err(|e| ExtismError::msg(format!("host_plugin_command: invalid input: {e}")))?;
1978
1979 let ctx = user_data.get()?;
1980 let ctx = ctx
1981 .lock()
1982 .map_err(|e| ExtismError::msg(format!("host_plugin_command: lock: {e}")))?;
1983
1984 const MAX_PLUGIN_COMMAND_DEPTH: u32 = 8;
1985
1986 let response = if ctx.plugin_command_depth >= MAX_PLUGIN_COMMAND_DEPTH {
1987 serde_json::json!({
1988 "success": false,
1989 "error": format!(
1990 "Cross-plugin command call depth limit exceeded (max {MAX_PLUGIN_COMMAND_DEPTH})"
1991 ),
1992 })
1993 } else if parsed.plugin_id.trim().is_empty() || parsed.command.trim().is_empty() {
1994 serde_json::json!({
1995 "success": false,
1996 "error": "plugin_id and command are required",
1997 })
1998 } else if parsed.plugin_id == ctx.plugin_id {
1999 serde_json::json!({
2000 "success": false,
2001 "error": "Plugins cannot call their own commands via host_plugin_command",
2002 })
2003 } else {
2004 let permission_target = format!("{}:{}", parsed.plugin_id, parsed.command);
2005 match ctx.check_perm(PermissionType::ExecuteCommands, &permission_target) {
2006 Ok(()) => match ctx.plugin_command_bridge.call(
2007 &ctx.plugin_id,
2008 &parsed.plugin_id,
2009 &parsed.command,
2010 parsed.params,
2011 ) {
2012 Ok(data) => serde_json::json!({
2013 "success": true,
2014 "data": data,
2015 }),
2016 Err(error) => serde_json::json!({
2017 "success": false,
2018 "error": error,
2019 }),
2020 },
2021 Err(error) => serde_json::json!({
2022 "success": false,
2023 "error": error.to_string(),
2024 }),
2025 }
2026 };
2027
2028 let json = serde_json::to_string(&response)
2029 .map_err(|e| ExtismError::msg(format!("host_plugin_command: serialize: {e}")))?;
2030 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
2031 Ok(())
2032}
2033
2034fn host_get_runtime_context(
2038 plugin: &mut CurrentPlugin,
2039 _inputs: &[Val],
2040 outputs: &mut [Val],
2041 user_data: UserData<HostContext>,
2042) -> Result<(), ExtismError> {
2043 let ctx = user_data.get()?;
2044 let ctx = ctx
2045 .lock()
2046 .map_err(|e| ExtismError::msg(format!("host_get_runtime_context: lock: {e}")))?;
2047 let json = serde_json::to_string(&ctx.runtime_context_provider.get_context(&ctx.plugin_id))
2048 .map_err(|e| ExtismError::msg(format!("host_get_runtime_context: serialize: {e}")))?;
2049 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
2050 Ok(())
2051}
2052
2053fn host_ws_request(
2059 plugin: &mut CurrentPlugin,
2060 inputs: &[Val],
2061 outputs: &mut [Val],
2062 user_data: UserData<HostContext>,
2063) -> Result<(), ExtismError> {
2064 let input: String = plugin.memory_get_val(&inputs[0])?;
2065 let ctx = user_data.get()?;
2066 let ctx = ctx
2067 .lock()
2068 .map_err(|e| ExtismError::msg(format!("host_ws_request: lock: {e}")))?;
2069 let result = match ctx.ws_bridge.request(&input) {
2072 Ok(s) => s,
2073 Err(e) => serde_json::json!({
2074 "ok": false,
2075 "error": format!("host_ws_request: {e}"),
2076 })
2077 .to_string(),
2078 };
2079 plugin.memory_set_val(&mut outputs[0], result.as_str())?;
2080 Ok(())
2081}
2082
2083fn host_hash_file(
2088 plugin: &mut CurrentPlugin,
2089 inputs: &[Val],
2090 outputs: &mut [Val],
2091 user_data: UserData<HostContext>,
2092) -> Result<(), ExtismError> {
2093 let input: String = plugin.memory_get_val(&inputs[0])?;
2094
2095 #[derive(serde::Deserialize)]
2096 struct HashInput {
2097 path: String,
2098 }
2099
2100 let parsed: HashInput = serde_json::from_str(&input)
2101 .map_err(|e| ExtismError::msg(format!("host_hash_file: invalid input: {e}")))?;
2102 let path = HostContext::validate_file_path(&parsed.path)?;
2103
2104 let ctx = user_data.get()?;
2105 let ctx = ctx
2106 .lock()
2107 .map_err(|e| ExtismError::msg(format!("host_hash_file: lock: {e}")))?;
2108 if ctx.check_perm(PermissionType::ReadFiles, &path).is_err() {
2110 plugin.memory_set_val(&mut outputs[0], "")?;
2111 return Ok(());
2112 }
2113
2114 let hash = match futures_lite::future::block_on(ctx.fs.hash_file(Path::new(&path))) {
2115 Ok(hash) => hash,
2116 Err(_) => {
2117 plugin.memory_set_val(&mut outputs[0], "")?;
2118 return Ok(());
2119 }
2120 };
2121
2122 let json = serde_json::json!({ "hash": hash }).to_string();
2123 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
2124 Ok(())
2125}
2126
2127#[cfg(feature = "http")]
2133fn host_proxy_request(
2134 plugin: &mut CurrentPlugin,
2135 inputs: &[Val],
2136 outputs: &mut [Val],
2137 user_data: UserData<HostContext>,
2138) -> Result<(), ExtismError> {
2139 let input: String = plugin.memory_get_val(&inputs[0])?;
2140
2141 #[derive(serde::Deserialize)]
2142 struct ProxyInput {
2143 proxy_id: String,
2144 #[serde(default)]
2145 path: String,
2146 #[serde(default = "default_method")]
2147 method: String,
2148 #[serde(default)]
2149 headers: std::collections::HashMap<String, String>,
2150 body: Option<String>,
2151 }
2152
2153 fn default_method() -> String {
2154 "POST".to_string()
2155 }
2156
2157 #[derive(serde::Serialize)]
2158 struct ProxyOutput {
2159 status: u16,
2160 headers: std::collections::HashMap<String, String>,
2161 body: String,
2162 #[serde(skip_serializing_if = "Option::is_none")]
2163 body_base64: Option<String>,
2164 }
2165
2166 fn err_envelope(msg: &str) -> String {
2169 serde_json::json!({
2170 "status": 0,
2171 "headers": {},
2172 "body": msg,
2173 "error": msg,
2174 })
2175 .to_string()
2176 }
2177
2178 let parsed: ProxyInput = match serde_json::from_str(&input) {
2179 Ok(p) => p,
2180 Err(e) => {
2181 let msg = format!("host_proxy_request: invalid input: {e}");
2182 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2183 return Ok(());
2184 }
2185 };
2186
2187 if let Err(e) = HostContext::validate_http_headers(&parsed.headers) {
2188 let msg = format!("host_proxy_request: {e}");
2189 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2190 return Ok(());
2191 }
2192
2193 let (server_url, auth_token) = {
2195 let ctx = user_data.get()?;
2196 let ctx = ctx
2197 .lock()
2198 .map_err(|e| ExtismError::msg(format!("host_proxy_request: lock: {e}")))?;
2199
2200 let runtime_json = ctx.runtime_context_provider.get_context(&ctx.plugin_id);
2201 let server_url = match runtime_json
2202 .get("server_url")
2203 .and_then(|v| v.as_str())
2204 .map(|s| s.trim_end_matches('/').to_string())
2205 {
2206 Some(url) => url,
2207 None => {
2208 let msg = "host_proxy_request: server_url not available in runtime context";
2209 plugin.memory_set_val(&mut outputs[0], err_envelope(msg).as_str())?;
2210 return Ok(());
2211 }
2212 };
2213 let auth_token = runtime_json
2214 .get("auth_token")
2215 .and_then(|v| v.as_str())
2216 .map(|s| s.to_string());
2217 (server_url, auth_token)
2218 };
2219
2220 let proxy_url = if parsed.path.is_empty() {
2222 format!("{}/api/proxy/{}", server_url, parsed.proxy_id)
2223 } else {
2224 format!(
2225 "{}/api/proxy/{}/{}",
2226 server_url,
2227 parsed.proxy_id,
2228 parsed.path.trim_start_matches('/')
2229 )
2230 };
2231
2232 let agent: ureq::Agent = ureq::Agent::config_builder()
2234 .timeout_global(Some(std::time::Duration::from_secs(120)))
2235 .http_status_as_error(false)
2236 .build()
2237 .into();
2238
2239 let mut request_builder = ureq::http::Request::builder()
2240 .method(parsed.method.as_str())
2241 .uri(proxy_url.as_str())
2242 .header("Content-Type", "application/json");
2243
2244 if let Some(ref token) = auth_token {
2245 request_builder = request_builder.header("Authorization", format!("Bearer {}", token));
2246 }
2247
2248 for (key, value) in &parsed.headers {
2249 request_builder = request_builder.header(key, value);
2250 }
2251
2252 let response = if let Some(body) = &parsed.body {
2253 match request_builder.body(body.clone()) {
2254 Ok(request) => match agent.run(request) {
2255 Ok(r) => r,
2256 Err(e) => {
2257 let msg = format!("host_proxy_request: {e}");
2258 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2259 return Ok(());
2260 }
2261 },
2262 Err(e) => {
2263 let msg = format!("host_proxy_request: build request: {e}");
2264 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2265 return Ok(());
2266 }
2267 }
2268 } else {
2269 match request_builder.body(()) {
2270 Ok(request) => match agent.run(request) {
2271 Ok(r) => r,
2272 Err(e) => {
2273 let msg = format!("host_proxy_request: {e}");
2274 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2275 return Ok(());
2276 }
2277 },
2278 Err(e) => {
2279 let msg = format!("host_proxy_request: build request: {e}");
2280 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2281 return Ok(());
2282 }
2283 }
2284 };
2285
2286 let status = response.status().as_u16();
2287 let mut resp_headers = std::collections::HashMap::new();
2288 for (name, value) in response.headers() {
2289 if let Ok(v) = value.to_str() {
2290 resp_headers.insert(name.to_string(), v.to_string());
2291 }
2292 }
2293 let mut response = response;
2294 let body_bytes = match response
2295 .body_mut()
2296 .with_config()
2297 .limit(128 * 1024 * 1024)
2298 .read_to_vec()
2299 {
2300 Ok(bytes) => bytes,
2301 Err(e) => {
2302 let msg = format!("host_proxy_request: read body: {e}");
2303 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2304 return Ok(());
2305 }
2306 };
2307 let body = String::from_utf8_lossy(&body_bytes).to_string();
2308 use base64::Engine as _;
2309 let body_base64 = Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes));
2310
2311 let output = ProxyOutput {
2312 status,
2313 headers: resp_headers,
2314 body,
2315 body_base64,
2316 };
2317
2318 let json = serde_json::to_string(&output)
2319 .map_err(|e| ExtismError::msg(format!("host_proxy_request: serialize: {e}")))?;
2320 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
2321 Ok(())
2322}
2323
2324#[cfg(feature = "http")]
2330fn host_http_request(
2331 plugin: &mut CurrentPlugin,
2332 inputs: &[Val],
2333 outputs: &mut [Val],
2334 user_data: UserData<HostContext>,
2335) -> Result<(), ExtismError> {
2336 use base64::Engine as _;
2337 use ureq::http::Request;
2338
2339 let input: String = plugin.memory_get_val(&inputs[0])?;
2340
2341 #[derive(serde::Deserialize)]
2342 struct HttpInput {
2343 url: String,
2344 method: String,
2345 headers: std::collections::HashMap<String, String>,
2346 body: Option<String>,
2347 body_base64: Option<String>,
2349 timeout_ms: Option<u64>,
2351 }
2352
2353 #[derive(serde::Serialize)]
2354 struct HttpOutput {
2355 status: u16,
2356 headers: std::collections::HashMap<String, String>,
2357 body: String,
2358 #[serde(skip_serializing_if = "Option::is_none")]
2359 body_base64: Option<String>,
2360 }
2361
2362 fn err_envelope(msg: &str) -> String {
2365 serde_json::json!({
2366 "status": 0,
2367 "headers": {},
2368 "body": msg,
2369 "error": msg,
2370 })
2371 .to_string()
2372 }
2373
2374 let parsed: HttpInput = match serde_json::from_str(&input) {
2375 Ok(p) => p,
2376 Err(e) => {
2377 let msg = format!("host_http_request: invalid input: {e}");
2378 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2379 return Ok(());
2380 }
2381 };
2382
2383 if let Err(e) = HostContext::validate_http_headers(&parsed.headers) {
2384 let msg = format!("host_http_request: {e}");
2385 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2386 return Ok(());
2387 }
2388
2389 {
2390 let ctx = user_data.get()?;
2391 let ctx = ctx
2392 .lock()
2393 .map_err(|e| ExtismError::msg(format!("host_http_request: lock: {e}")))?;
2394 if let Err(e) = ctx.check_perm(PermissionType::HttpRequests, &parsed.url) {
2395 let msg = format!("host_http_request: {e}");
2396 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2397 return Ok(());
2398 }
2399 }
2400
2401 const MIN_HTTP_TIMEOUT_MS: u64 = 1_000;
2402 const MAX_HTTP_TIMEOUT_MS: u64 = 300_000;
2403
2404 let timeout = parsed
2405 .timeout_ms
2406 .map(|value| value.clamp(MIN_HTTP_TIMEOUT_MS, MAX_HTTP_TIMEOUT_MS))
2407 .map(std::time::Duration::from_millis);
2408 let agent: ureq::Agent = ureq::Agent::config_builder()
2409 .timeout_global(timeout)
2410 .http_status_as_error(false)
2411 .build()
2412 .into();
2413
2414 let mut request_builder = Request::builder()
2415 .method(parsed.method.as_str())
2416 .uri(parsed.url.as_str());
2417 for (key, value) in &parsed.headers {
2418 request_builder = request_builder.header(key, value);
2419 }
2420
2421 let response = if let Some(b64) = &parsed.body_base64 {
2422 let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
2423 Ok(bytes) => bytes,
2424 Err(e) => {
2425 let msg = format!("host_http_request: base64 decode: {e}");
2426 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2427 return Ok(());
2428 }
2429 };
2430 match request_builder.body(bytes) {
2431 Ok(request) => match agent.run(request) {
2432 Ok(r) => r,
2433 Err(e) => {
2434 let msg = format!("host_http_request: {e}");
2435 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2436 return Ok(());
2437 }
2438 },
2439 Err(e) => {
2440 let msg = format!("host_http_request: invalid request: {e}");
2441 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2442 return Ok(());
2443 }
2444 }
2445 } else if let Some(body) = &parsed.body {
2446 match request_builder.body(body.clone()) {
2447 Ok(request) => match agent.run(request) {
2448 Ok(r) => r,
2449 Err(e) => {
2450 let msg = format!("host_http_request: {e}");
2451 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2452 return Ok(());
2453 }
2454 },
2455 Err(e) => {
2456 let msg = format!("host_http_request: invalid request: {e}");
2457 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2458 return Ok(());
2459 }
2460 }
2461 } else {
2462 match request_builder.body(()) {
2463 Ok(request) => match agent.run(request) {
2464 Ok(r) => r,
2465 Err(e) => {
2466 let msg = format!("host_http_request: {e}");
2467 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2468 return Ok(());
2469 }
2470 },
2471 Err(e) => {
2472 let msg = format!("host_http_request: invalid request: {e}");
2473 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2474 return Ok(());
2475 }
2476 }
2477 };
2478
2479 let status = response.status().as_u16();
2480 if status >= 400 {
2481 log::warn!(
2482 "host_http_request: {} {} → {} (plugin={})",
2483 parsed.method,
2484 parsed.url,
2485 status,
2486 {
2487 let ctx = user_data.get().ok();
2488 ctx.and_then(|c| c.lock().ok().map(|g| g.plugin_id.clone()))
2489 .unwrap_or_default()
2490 },
2491 );
2492 }
2493 let mut resp_headers = std::collections::HashMap::new();
2494 for (name, value) in response.headers() {
2495 if let Ok(value) = value.to_str() {
2496 resp_headers.insert(name.to_string(), value.to_string());
2497 }
2498 }
2499 let mut response = response;
2500 let body_bytes = match response
2503 .body_mut()
2504 .with_config()
2505 .limit(128 * 1024 * 1024)
2506 .read_to_vec()
2507 {
2508 Ok(bytes) => bytes,
2509 Err(e) => {
2510 let msg = format!("host_http_request: read body: {e}");
2511 plugin.memory_set_val(&mut outputs[0], err_envelope(&msg).as_str())?;
2512 return Ok(());
2513 }
2514 };
2515 let body = String::from_utf8_lossy(&body_bytes).to_string();
2516 let body_base64 = Some(base64::engine::general_purpose::STANDARD.encode(&body_bytes));
2517
2518 let output = HttpOutput {
2519 status,
2520 headers: resp_headers,
2521 body,
2522 body_base64,
2523 };
2524
2525 let json = serde_json::to_string(&output)
2526 .map_err(|e| ExtismError::msg(format!("host_http_request: serialize: {e}")))?;
2527
2528 plugin.memory_set_val(&mut outputs[0], json.as_str())?;
2529 Ok(())
2530}
2531
2532#[cfg(not(feature = "http"))]
2534fn host_http_request(
2535 plugin: &mut CurrentPlugin,
2536 _inputs: &[Val],
2537 outputs: &mut [Val],
2538 _user_data: UserData<HostContext>,
2539) -> Result<(), ExtismError> {
2540 let error = serde_json::json!({
2541 "status": 0,
2542 "headers": {},
2543 "body": "host_http_request: http feature not enabled"
2544 });
2545 plugin.memory_set_val(&mut outputs[0], error.to_string().as_str())?;
2546 Ok(())
2547}
2548
2549#[cfg(not(feature = "http"))]
2550fn host_proxy_request(
2551 plugin: &mut CurrentPlugin,
2552 _inputs: &[Val],
2553 outputs: &mut [Val],
2554 _user_data: UserData<HostContext>,
2555) -> Result<(), ExtismError> {
2556 let error = serde_json::json!({
2557 "status": 0,
2558 "headers": {},
2559 "body": "host_proxy_request: http feature not enabled"
2560 });
2561 plugin.memory_set_val(&mut outputs[0], error.to_string().as_str())?;
2562 Ok(())
2563}
2564
2565fn host_namespace_put_object(
2567 plugin: &mut CurrentPlugin,
2568 inputs: &[Val],
2569 outputs: &mut [Val],
2570 user_data: UserData<HostContext>,
2571) -> Result<(), ExtismError> {
2572 use base64::Engine as _;
2573
2574 let input: String = plugin.memory_get_val(&inputs[0])?;
2575
2576 #[derive(serde::Deserialize)]
2577 struct Input {
2578 ns_id: String,
2579 key: String,
2580 body_base64: String,
2581 mime_type: String,
2582 #[serde(default)]
2583 audience: Option<String>,
2584 }
2585
2586 let parsed: Input = serde_json::from_str(&input)
2587 .map_err(|e| ExtismError::msg(format!("host_namespace_put_object: invalid input: {e}")))?;
2588
2589 let bytes = base64::engine::general_purpose::STANDARD
2590 .decode(&parsed.body_base64)
2591 .map_err(|e| ExtismError::msg(format!("host_namespace_put_object: base64 decode: {e}")))?;
2592
2593 let ctx = user_data.get()?;
2594 let ctx = ctx
2595 .lock()
2596 .map_err(|e| ExtismError::msg(format!("host_namespace_put_object: lock: {e}")))?;
2597 let result = ctx.namespace_provider.put_object(
2598 &parsed.ns_id,
2599 &parsed.key,
2600 &bytes,
2601 &parsed.mime_type,
2602 parsed.audience.as_deref(),
2603 );
2604
2605 let json = match result {
2606 Ok(()) => serde_json::json!({ "ok": true }),
2607 Err(e) => serde_json::json!({ "error": e }),
2608 };
2609 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2610 Ok(())
2611}
2612
2613fn host_namespace_get_object(
2615 plugin: &mut CurrentPlugin,
2616 inputs: &[Val],
2617 outputs: &mut [Val],
2618 user_data: UserData<HostContext>,
2619) -> Result<(), ExtismError> {
2620 use base64::Engine as _;
2621
2622 let input: String = plugin.memory_get_val(&inputs[0])?;
2623
2624 #[derive(serde::Deserialize)]
2625 struct Input {
2626 ns_id: String,
2627 key: String,
2628 }
2629
2630 let parsed: Input = serde_json::from_str(&input)
2631 .map_err(|e| ExtismError::msg(format!("host_namespace_get_object: invalid input: {e}")))?;
2632
2633 let ctx = user_data.get()?;
2634 let ctx = ctx
2635 .lock()
2636 .map_err(|e| ExtismError::msg(format!("host_namespace_get_object: lock: {e}")))?;
2637 let result = ctx
2638 .namespace_provider
2639 .get_object(&parsed.ns_id, &parsed.key);
2640
2641 let json = match result {
2642 Ok(bytes) => {
2643 let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
2644 serde_json::json!({ "data": encoded })
2645 }
2646 Err(e) => serde_json::json!({ "error": e }),
2647 };
2648 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2649 Ok(())
2650}
2651
2652fn host_namespace_get_objects_batch(
2654 plugin: &mut CurrentPlugin,
2655 inputs: &[Val],
2656 outputs: &mut [Val],
2657 user_data: UserData<HostContext>,
2658) -> Result<(), ExtismError> {
2659 use base64::Engine as _;
2660
2661 let input: String = plugin.memory_get_val(&inputs[0])?;
2662
2663 #[derive(serde::Deserialize)]
2664 struct Input {
2665 ns_id: String,
2666 keys: Vec<String>,
2667 }
2668
2669 let parsed: Input = serde_json::from_str(&input).map_err(|e| {
2670 ExtismError::msg(format!(
2671 "host_namespace_get_objects_batch: invalid input: {e}"
2672 ))
2673 })?;
2674
2675 let ctx = user_data.get()?;
2676 let ctx = ctx
2677 .lock()
2678 .map_err(|e| ExtismError::msg(format!("host_namespace_get_objects_batch: lock: {e}")))?;
2679
2680 let result = ctx
2681 .namespace_provider
2682 .get_objects_batch(&parsed.ns_id, &parsed.keys);
2683
2684 let json = match result {
2685 Ok(batch) => {
2686 let objects: serde_json::Map<String, serde_json::Value> = batch
2687 .objects
2688 .into_iter()
2689 .map(|(key, entry)| {
2690 let is_text = entry.mime_type.starts_with("text/");
2691 let val = if is_text {
2692 serde_json::json!({
2693 "data": String::from_utf8_lossy(&entry.bytes),
2694 "mime_type": entry.mime_type,
2695 "encoding": "text",
2696 })
2697 } else {
2698 serde_json::json!({
2699 "data": base64::engine::general_purpose::STANDARD.encode(&entry.bytes),
2700 "mime_type": entry.mime_type,
2701 "encoding": "base64",
2702 })
2703 };
2704 (key, val)
2705 })
2706 .collect();
2707
2708 let mut resp = serde_json::json!({ "objects": objects });
2709 if !batch.errors.is_empty() {
2710 resp["errors"] = serde_json::json!(batch.errors);
2711 }
2712 resp
2713 }
2714 Err(e) => serde_json::json!({ "error": e }),
2715 };
2716 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2717 Ok(())
2718}
2719
2720fn host_namespace_delete_object(
2722 plugin: &mut CurrentPlugin,
2723 inputs: &[Val],
2724 outputs: &mut [Val],
2725 user_data: UserData<HostContext>,
2726) -> Result<(), ExtismError> {
2727 let input: String = plugin.memory_get_val(&inputs[0])?;
2728
2729 #[derive(serde::Deserialize)]
2730 struct Input {
2731 ns_id: String,
2732 key: String,
2733 }
2734
2735 let parsed: Input = serde_json::from_str(&input).map_err(|e| {
2736 ExtismError::msg(format!("host_namespace_delete_object: invalid input: {e}"))
2737 })?;
2738
2739 let ctx = user_data.get()?;
2740 let ctx = ctx
2741 .lock()
2742 .map_err(|e| ExtismError::msg(format!("host_namespace_delete_object: lock: {e}")))?;
2743 let result = ctx
2744 .namespace_provider
2745 .delete_object(&parsed.ns_id, &parsed.key);
2746
2747 let json = match result {
2748 Ok(()) => serde_json::json!({ "ok": true }),
2749 Err(e) => serde_json::json!({ "error": e }),
2750 };
2751 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2752 Ok(())
2753}
2754
2755fn host_namespace_list_objects(
2757 plugin: &mut CurrentPlugin,
2758 inputs: &[Val],
2759 outputs: &mut [Val],
2760 user_data: UserData<HostContext>,
2761) -> Result<(), ExtismError> {
2762 let input: String = plugin.memory_get_val(&inputs[0])?;
2763
2764 #[derive(serde::Deserialize)]
2765 struct Input {
2766 ns_id: String,
2767 #[serde(default)]
2768 prefix: Option<String>,
2769 #[serde(default)]
2770 limit: Option<u32>,
2771 #[serde(default)]
2772 offset: Option<u32>,
2773 }
2774
2775 let parsed: Input = serde_json::from_str(&input).map_err(|e| {
2776 ExtismError::msg(format!("host_namespace_list_objects: invalid input: {e}"))
2777 })?;
2778
2779 let ctx = user_data.get()?;
2780 let ctx = ctx
2781 .lock()
2782 .map_err(|e| ExtismError::msg(format!("host_namespace_list_objects: lock: {e}")))?;
2783 let result = ctx.namespace_provider.list_objects(
2784 &parsed.ns_id,
2785 parsed.prefix.as_deref(),
2786 parsed.limit,
2787 parsed.offset,
2788 );
2789
2790 let json = match result {
2791 Ok(objects) => serde_json::to_value(&objects).unwrap_or(serde_json::json!([])),
2792 Err(e) => serde_json::json!({ "error": e }),
2793 };
2794 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2795 Ok(())
2796}
2797
2798fn host_namespace_list(
2800 plugin: &mut CurrentPlugin,
2801 inputs: &[Val],
2802 outputs: &mut [Val],
2803 user_data: UserData<HostContext>,
2804) -> Result<(), ExtismError> {
2805 let _input: String = plugin.memory_get_val(&inputs[0])?;
2806
2807 let ctx = user_data.get()?;
2808 let ctx = ctx
2809 .lock()
2810 .map_err(|e| ExtismError::msg(format!("host_namespace_list: lock: {e}")))?;
2811 let result = ctx.namespace_provider.list_namespaces();
2812
2813 let json = match result {
2814 Ok(entries) => serde_json::to_value(&entries).unwrap_or(serde_json::json!([])),
2815 Err(e) => serde_json::json!({ "error": e }),
2816 };
2817 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2818 Ok(())
2819}
2820
2821fn host_namespace_create(
2823 plugin: &mut CurrentPlugin,
2824 inputs: &[Val],
2825 outputs: &mut [Val],
2826 user_data: UserData<HostContext>,
2827) -> Result<(), ExtismError> {
2828 let input: String = plugin.memory_get_val(&inputs[0])?;
2829
2830 #[derive(serde::Deserialize)]
2831 struct Input {
2832 #[serde(default)]
2833 metadata: Option<serde_json::Value>,
2834 }
2835
2836 let parsed: Input = serde_json::from_str(&input)
2837 .map_err(|e| ExtismError::msg(format!("host_namespace_create: invalid input: {e}")))?;
2838
2839 let ctx = user_data.get()?;
2840 let ctx = ctx
2841 .lock()
2842 .map_err(|e| ExtismError::msg(format!("host_namespace_create: lock: {e}")))?;
2843 let result = ctx
2844 .namespace_provider
2845 .create_namespace(parsed.metadata.as_ref());
2846
2847 let json = match result {
2848 Ok(entry) => serde_json::to_value(&entry).unwrap_or_else(|_| serde_json::json!({})),
2849 Err(e) => serde_json::json!({ "error": e }),
2850 };
2851 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2852 Ok(())
2853}
2854
2855fn host_namespace_sync_audience(
2857 plugin: &mut CurrentPlugin,
2858 inputs: &[Val],
2859 outputs: &mut [Val],
2860 user_data: UserData<HostContext>,
2861) -> Result<(), ExtismError> {
2862 let input: String = plugin.memory_get_val(&inputs[0])?;
2863
2864 #[derive(serde::Deserialize)]
2865 struct Input {
2866 ns_id: String,
2867 audience: String,
2868 access: String,
2869 }
2870
2871 let parsed: Input = serde_json::from_str(&input).map_err(|e| {
2872 ExtismError::msg(format!("host_namespace_sync_audience: invalid input: {e}"))
2873 })?;
2874
2875 let ctx = user_data.get()?;
2876 let ctx = ctx
2877 .lock()
2878 .map_err(|e| ExtismError::msg(format!("host_namespace_sync_audience: lock: {e}")))?;
2879 let result =
2880 ctx.namespace_provider
2881 .sync_audience(&parsed.ns_id, &parsed.audience, &parsed.access);
2882
2883 let json = match result {
2884 Ok(()) => serde_json::json!({ "ok": true }),
2885 Err(e) => serde_json::json!({ "error": e }),
2886 };
2887 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2888 Ok(())
2889}
2890
2891fn host_namespace_send_email(
2893 plugin: &mut CurrentPlugin,
2894 inputs: &[Val],
2895 outputs: &mut [Val],
2896 user_data: UserData<HostContext>,
2897) -> Result<(), ExtismError> {
2898 let input: String = plugin.memory_get_val(&inputs[0])?;
2899
2900 #[derive(serde::Deserialize)]
2901 struct Input {
2902 ns_id: String,
2903 audience: String,
2904 subject: String,
2905 reply_to: Option<String>,
2906 }
2907
2908 let parsed: Input = serde_json::from_str(&input)
2909 .map_err(|e| ExtismError::msg(format!("host_namespace_send_email: invalid input: {e}")))?;
2910
2911 let ctx = user_data.get()?;
2912 let ctx = ctx
2913 .lock()
2914 .map_err(|e| ExtismError::msg(format!("host_namespace_send_email: lock: {e}")))?;
2915 let result = ctx.namespace_provider.send_audience_email(
2916 &parsed.ns_id,
2917 &parsed.audience,
2918 &parsed.subject,
2919 parsed.reply_to.as_deref(),
2920 );
2921
2922 let json = match result {
2923 Ok(val) => val,
2924 Err(e) => serde_json::json!({ "error": e }),
2925 };
2926 plugin.memory_set_val(&mut outputs[0], json.to_string().as_str())?;
2927 Ok(())
2928}