Skip to main content

netget/server/xmlrpc/
mod.rs

1//! XML-RPC server implementation
2//!
3//! This module implements an XML-RPC server over HTTP that allows LLM control
4//! over RPC method execution, introspection, and response generation.
5//!
6//! XML-RPC specification: http://xmlrpc.com/spec.md
7//!
8//! The LLM controls:
9//! - Method execution (custom methods defined by user prompt)
10//! - Introspection responses (system.listMethods, system.methodHelp, etc.)
11//! - Fault generation for errors
12//! - Extensions (nil values, i8/64-bit integers, system.multicall)
13
14pub mod actions;
15
16use anyhow::Result;
17use bytes::Bytes;
18use http_body_util::{BodyExt, Full};
19use hyper::{Request, Response};
20use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event as XmlEvent};
21use quick_xml::{Reader, Writer};
22use std::io::Cursor;
23use std::net::SocketAddr;
24use std::sync::Arc;
25use tokio::sync::mpsc;
26
27use crate::llm::action_helper::call_llm;
28use crate::llm::ollama_client::OllamaClient;
29use crate::llm::ActionResult;
30#[cfg(not(target_arch = "wasm32"))]
31use crate::logging::emit::Log;
32use crate::server::connection::ConnectionId;
33use crate::state::app_state::AppState;
34use crate::state::server::{
35    ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo, ServerId,
36};
37
38pub use actions::XmlRpcProtocol;
39
40/// Largest request body accepted, in bytes. hyper imposes no limit of its own, and
41/// the body is buffered whole, copied into a `String` and traced in full.
42#[cfg(not(target_arch = "wasm32"))]
43pub const MAX_REQUEST_BODY_BYTES: usize = 4 * 1024 * 1024;
44
45/// How long a peer that has connected and produced nothing may hold a slot.
46///
47/// HTTP is client-speaks-first: the server says nothing until a request line arrives, so a peer
48/// that has completed the TCP handshake and sent no byte has asked nothing and negotiated
49/// nothing. That state carries no protocol yet, which is why this number is the same across
50/// netget's HTTP-shaped servers while the idle bound below is not. Apache's `mod_reqtimeout`
51/// gives the request header 20s and nginx's `client_header_timeout` 60s; 30s sits between the
52/// two deployed norms.
53///
54/// hyper's own `header_read_timeout` is **not** this bound: its 30-second default is inert
55/// unless `http1::Builder::timer` is also set, which nothing here does — hyper downgrades a
56/// defaulted duration to `None` when no timer is present and applies no deadline at all.
57#[cfg(not(target_arch = "wasm32"))]
58const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
59
60/// How long a connection with nothing outstanding may sit idle between requests.
61///
62/// XML-RPC's canonical client does not keep connections alive at all: Python's
63/// `xmlrpc.client.ServerProxy` opens a connection per call and closes it, and the same is true
64/// of most of the transports built on the pattern. So there is no legitimate long idle window
65/// to protect, and this is a backstop for the minority of transports that do reuse rather than
66/// a keep-alive allowance — 60s, an order of magnitude past a pooled client's round trip and
67/// well short of letting an abandoned connection sit for minutes. A request still being answered
68/// is not silence: the watchdog reads
69/// [`ConnectionActivity`](crate::server::accept_bounded::ConnectionActivity), which reports a
70/// connection with work in flight — a model round-trip, or an event a `manual` rule parked for a
71/// human — as not idle at all.
72#[cfg(not(target_arch = "wasm32"))]
73const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
74
75/// Concurrent connections this server admits.
76///
77/// The shared default. Each admitted connection may buffer one body of up to
78/// [`MAX_REQUEST_BODY_BYTES`] (4 MiB), so the cap is what turns that per-connection bound into a total
79/// one and holds the worst case well inside the ~1 GiB ceiling netget's HTTP family is held
80/// to. A protocol only declares a smaller number when its per-connection cost is larger.
81#[cfg(not(target_arch = "wasm32"))]
82const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
83
84/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
85///
86/// `503 Service Unavailable` with a `Retry-After`, written directly onto the socket because the
87/// peer has not sent a request line for hyper to answer. Fixed bytes: nothing derived from an
88/// error reaches the wire (see `crate::utils::wire_failure`).
89#[cfg(not(target_arch = "wasm32"))]
90const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
91    Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
92
93/// XML-RPC fault codes used for netget's own failures.
94///
95/// The peer gets a category, never the error: an LLM backend URL, a model name or an
96/// `anyhow` context chain is for the log, not for a stranger's client. See
97/// `src/utils/wire_failure.rs`.
98///
99/// `-32000..=-32099` is the implementation-defined server-error range in the XML-RPC
100/// fault-code interop note, so a saturated backend gets its own retryable code instead of
101/// being flattened into the permanent `-32603 internal error`.
102#[cfg(not(target_arch = "wasm32"))]
103const FAULT_INTERNAL_ERROR: i32 = -32603;
104/// Backend saturated — transient, the client should back off and retry.
105#[cfg(not(target_arch = "wasm32"))]
106const FAULT_SERVER_BUSY: i32 = -32000;
107
108/// XML-RPC server that handles RPC method calls with LLM
109pub struct XmlRpcServer;
110
111#[cfg(not(target_arch = "wasm32"))]
112impl XmlRpcServer {
113    /// Spawn XML-RPC server with integrated LLM actions
114    pub async fn spawn_with_llm_actions(
115        listen_addr: SocketAddr,
116        llm_client: OllamaClient,
117        app_state: Arc<AppState>,
118        status_tx: mpsc::UnboundedSender<String>,
119        server_id: ServerId,
120    ) -> Result<SocketAddr> {
121        let listener =
122            crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
123        let local_addr = listener.local_addr()?;
124        Log::new(Some(&status_tx)).info(format!("XML-RPC server listening on {}", local_addr));
125
126        let protocol = Arc::new(XmlRpcProtocol::new());
127
128        let task_registrar = app_state.clone();
129        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
130        let accept_handle = tokio::spawn(async move {
131            loop {
132                match crate::server::accept_bounded::accept_bounded(
133                    &listener,
134                    &limiter,
135                    CONNECTION_CAP_REFUSAL,
136                    "XML-RPC",
137                    Some(&status_tx),
138                )
139                .await
140                {
141                    Ok((stream, remote_addr, permit)) => {
142                        let connection_id =
143                            ConnectionId::new(app_state.get_next_unified_id().await);
144                        Log::new(Some(&status_tx)).info(format!(
145                            "XML-RPC connection {} from {}",
146                            connection_id, remote_addr
147                        ));
148
149                        // Track connection in server state
150                        let local_addr_conn = stream.local_addr().unwrap_or(listen_addr);
151                        let now = crate::utils::clock::Instant::now();
152
153                        let conn_state = ServerConnectionState {
154                            id: connection_id,
155                            remote_addr,
156                            local_addr: local_addr_conn,
157                            bytes_sent: 0,
158                            bytes_received: 0,
159                            packets_sent: 0,
160                            packets_received: 0,
161                            last_activity: now,
162                            status: ConnectionStatus::Active,
163                            status_changed_at: now,
164                            protocol_info: ProtocolConnectionInfo::empty(),
165                        };
166
167                        app_state
168                            .add_connection_to_server(server_id, conn_state)
169                            .await;
170                        let _ = status_tx.send("__UPDATE_UI__".to_string());
171
172                        let llm_clone = llm_client.clone();
173                        let state_clone = app_state.clone();
174                        let status_clone = status_tx.clone();
175                        let protocol_clone = protocol.clone();
176
177                        // Spawn connection handler
178                        // Tracked, not detached: stop_server must abort this task too.
179                        let task_owner = app_state.clone();
180                        task_owner
181                            .spawn_server_task(server_id, async move {
182                                // Held for the life of the connection, so the cap counts live
183                                // clients rather than accepts.
184                                let _permit = permit;
185
186                                // First-byte bound, before hyper sees the socket.
187                                //
188                                // hyper owns every read once `serve_connection` starts, and a
189                                // deadline on those reads would be wrong here rather than merely
190                                // awkward: hyper keeps polling the connection for more input
191                                // while a request is being answered, so such a deadline would
192                                // fire in the middle of a model round-trip. `peek` waits for data
193                                // without consuming it, so the request line is still there for
194                                // hyper afterwards, and it bounds exactly the case that needs
195                                // bounding - a peer that has connected and sent nothing at all.
196                                let spoke = matches!(
197                                    tokio::time::timeout(
198                                        FIRST_BYTE_READ_TIMEOUT,
199                                        stream.peek(&mut [0u8; 1]),
200                                    )
201                                    .await,
202                                    Ok(Ok(n)) if n > 0
203                                );
204
205                                let io = hyper_util::rt::TokioIo::new(stream);
206
207                                // Tracks whether this connection is answering anything. A
208                                // request waiting on the model, or parked for a human by a
209                                // `manual` rule, holds the count above zero, so the idle watchdog
210                                // below cannot close the connection the answer belongs to however
211                                // long it takes - only genuine silence counts.
212                                let activity = std::sync::Arc::new(
213                                    crate::server::accept_bounded::ConnectionActivity::new(),
214                                );
215                                let activity_for_service = std::sync::Arc::clone(&activity);
216
217                                // Create service function for this connection
218                                let service = hyper::service::service_fn(|req| {
219                                    let llm = llm_clone.clone();
220                                    let state = state_clone.clone();
221                                    let status = status_clone.clone();
222                                    let proto = protocol_clone.clone();
223                                    let activity = std::sync::Arc::clone(&activity_for_service);
224                                    async move {
225                                        let _busy = activity.busy();
226                                        handle_xmlrpc_request(
227                                            req,
228                                            connection_id,
229                                            server_id,
230                                            remote_addr,
231                                            llm,
232                                            state,
233                                            status,
234                                            proto,
235                                        )
236                                        .await
237                                    }
238                                });
239
240                                // Serve HTTP/1 on this connection, bounded at both ends.
241                                if !spoke {
242                                    Log::new(Some(&status_clone)).debug(format!(
243                                        "XML-RPC peer {} sent nothing for {}s; closing before \
244                                         any request",
245                                        remote_addr,
246                                        FIRST_BYTE_READ_TIMEOUT.as_secs()
247                                    ));
248                                } else {
249                                    let conn = hyper::server::conn::http1::Builder::new()
250                                        .serve_connection(io, service);
251                                    tokio::pin!(conn);
252                                    tokio::select! {
253                                        result = &mut conn => {
254                                            if let Err(err) = result {
255                                                Log::new(Some(&status_clone)).error(
256                                                    format!(
257                                                        "XML-RPC connection {} error: \
258                                                         {}",
259                                                        connection_id, err
260                                                    ),
261                                                );
262                                            }
263                                        }
264                                        _ = crate::server::accept_bounded::watch_idle(
265                                            std::sync::Arc::clone(&activity),
266                                            IDLE_BETWEEN_REQUESTS_TIMEOUT,
267                                        ) => {
268                                            Log::new(Some(&status_clone)).debug(format!(
269                                                "XML-RPC connection {} idle for {}s; closing",
270                                                connection_id,
271                                                IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
272                                            ));
273                                        }
274                                    }
275                                }
276
277                                // Mark connection as closed
278                                state_clone
279                                    .close_connection_on_server(server_id, connection_id)
280                                    .await;
281                                Log::new(Some(&status_clone))
282                                    .info(format!("XML-RPC connection {} closed", connection_id));
283                                let _ = status_clone.send("__UPDATE_UI__".to_string());
284                            })
285                            .await;
286                    }
287                    Err(e) => {
288                        Log::new(Some(&status_tx))
289                            .error(format!("Failed to accept XML-RPC connection: {}", e));
290                        break;
291                    }
292                }
293            }
294        });
295
296        task_registrar
297            .register_server_task(server_id, accept_handle)
298            .await;
299
300        Ok(local_addr)
301    }
302}
303
304/// Handle a single XML-RPC request
305#[cfg(not(target_arch = "wasm32"))]
306async fn handle_xmlrpc_request(
307    req: Request<hyper::body::Incoming>,
308    connection_id: ConnectionId,
309    server_id: ServerId,
310    remote_addr: SocketAddr,
311    llm_client: OllamaClient,
312    app_state: Arc<AppState>,
313    status_tx: mpsc::UnboundedSender<String>,
314    protocol: Arc<XmlRpcProtocol>,
315) -> Result<Response<Full<Bytes>>> {
316    // Collect request body, capped: hyper imposes no limit, and the body was
317    // buffered whole, copied into a String and then traced in full.
318    let (parts, body) = req.into_parts();
319    let body_bytes = http_body_util::Limited::new(body, MAX_REQUEST_BODY_BYTES)
320        .collect()
321        .await
322        .map_err(|e| anyhow::anyhow!("Failed to read request body: {}", e))?
323        .to_bytes();
324
325    let body_str = String::from_utf8_lossy(&body_bytes);
326
327    let log = Log::new(Some(&status_tx));
328    log.debug(format!(
329        "XML-RPC request: {} {} ({} bytes)",
330        parts.method,
331        parts.uri,
332        body_bytes.len()
333    ));
334
335    // Full request body, file only.
336    log.trace(format!("XML-RPC request body:\n{}", body_str));
337
338    // Check if it's POST (XML-RPC requires POST)
339    if parts.method != hyper::Method::POST {
340        let fault_xml = generate_fault(-32600, "Invalid request: XML-RPC requires POST method");
341        Log::new(Some(&status_tx)).debug(format!(
342            "XML-RPC error: invalid method {} (expected POST)",
343            parts.method
344        ));
345        return Ok(Response::builder()
346            .status(200)
347            .header("Content-Type", "text/xml; charset=utf-8")
348            .body(Full::new(Bytes::from(fault_xml)))
349            .unwrap());
350    }
351
352    // Parse XML-RPC methodCall
353    let method_call = match parse_method_call(&body_str) {
354        Ok(call) => call,
355        Err(e) => {
356            tracing::warn!("XML-RPC parse error (decision=reject_malformed_request): {e:#}");
357            Log::new(Some(&status_tx)).warn(format!("XML-RPC parse error: {}", e));
358            // The codec's message names library internals; the peer learns only that its
359            // document did not parse.
360            let fault_xml = generate_fault(-32700, "Parse error: malformed XML-RPC methodCall");
361            return Ok(Response::builder()
362                .status(200)
363                .header("Content-Type", "text/xml; charset=utf-8")
364                .body(Full::new(Bytes::from(fault_xml)))
365                .unwrap());
366        }
367    };
368
369    Log::new(Some(&status_tx)).debug(format!(
370        "XML-RPC method: {} ({} params)",
371        method_call.method_name,
372        method_call.params.len()
373    ));
374
375    // Create event for LLM
376    let event = actions::create_method_call_event(&method_call);
377
378    // Call LLM to get response
379    let execution_result = match call_llm(
380        &llm_client,
381        &app_state,
382        server_id,
383        Some(connection_id),
384        &event,
385        protocol.as_ref(),
386    )
387    .await
388    {
389        Ok(result) => result,
390        Err(e) => {
391            // Three cases must stay apart in the log: the model rejected (it emits its own
392            // `xmlrpc_fault_response`, so it never reaches here), the model answered nothing
393            // (below), and the LLM call itself errored (here).
394            let failure = crate::utils::WireFailure::classify(&e);
395            let (fault_code, decision) = if failure.is_overloaded() {
396                (FAULT_SERVER_BUSY, "fail_closed_llm_overloaded")
397            } else {
398                (FAULT_INTERNAL_ERROR, "fail_closed_llm_error")
399            };
400            tracing::error!(
401                "XML-RPC LLM error (decision={decision}, fault_code={fault_code}): {e:#}"
402            );
403            Log::new(Some(&status_tx))
404                .warn(format!("XML-RPC LLM error (decision={}): {}", decision, e));
405            // Category only — never the error text.
406            let fault_xml = generate_fault(fault_code, failure.text());
407            return Ok(Response::builder()
408                .status(200)
409                .header("Content-Type", "text/xml; charset=utf-8")
410                .body(Full::new(Bytes::from(fault_xml)))
411                .unwrap());
412        }
413    };
414
415    // Display messages from LLM
416    for msg in execution_result.messages {
417        let _ = status_tx.send(msg);
418    }
419
420    // Take the first XML document any action produced. ActionResult::Multiple is
421    // unwrapped: a nested Output used to fall through to "no response generated".
422    fn first_output(result: &ActionResult) -> Option<String> {
423        match result {
424            ActionResult::Output(bytes) => Some(String::from_utf8_lossy(bytes).to_string()),
425            ActionResult::Multiple(inner) => inner.iter().find_map(first_output),
426            _ => None,
427        }
428    }
429    let mut response_xml = execution_result
430        .protocol_results
431        .iter()
432        .find_map(first_output)
433        .unwrap_or_default();
434
435    // If no XML response was generated, return a fault
436    if response_xml.is_empty() {
437        tracing::warn!(
438            "XML-RPC: model produced no response action for method {} (decision=fail_closed_no_action, fault_code={})",
439            method_call.method_name,
440            FAULT_INTERNAL_ERROR
441        );
442        Log::new(Some(&status_tx)).warn(
443            "XML-RPC: LLM did not generate a response (decision=fail_closed_no_action)".to_string(),
444        );
445        response_xml = generate_fault(
446            FAULT_INTERNAL_ERROR,
447            crate::utils::WireFailure::Unavailable.text(),
448        );
449    }
450
451    let log = Log::new(Some(&status_tx));
452    log.trace(format!("XML-RPC response:\n{}", response_xml));
453    log.debug(format!(
454        "XML-RPC {} -> {} bytes",
455        method_call.method_name,
456        response_xml.len()
457    ));
458
459    Ok(Response::builder()
460        .status(200)
461        .header("Content-Type", "text/xml; charset=utf-8")
462        .body(Full::new(Bytes::from(response_xml)))
463        .unwrap())
464}
465
466/// XML-RPC method call structure
467#[derive(Debug, Clone)]
468pub struct MethodCall {
469    pub method_name: String,
470    pub params: Vec<XmlRpcValue>,
471}
472
473/// XML-RPC value types
474#[derive(Debug, Clone, PartialEq)]
475pub enum XmlRpcValue {
476    Int(i32),
477    I8(i64), // Extension: 64-bit integer
478    Boolean(bool),
479    String(String),
480    Double(f64),
481    DateTime(String), // ISO 8601 format
482    Base64(Vec<u8>),
483    Array(Vec<XmlRpcValue>),
484    Struct(Vec<(String, XmlRpcValue)>), // key-value pairs
485    Nil,                                // Extension: null value
486}
487
488/// Maximum `<array>`/`<struct>`/`<value>` nesting accepted from a client.
489///
490/// The parser is iterative, so *parsing* a deep document cannot overflow the stack — but the
491/// `XmlRpcValue` it returns is recursive, and the request path walks it recursively
492/// (`actions::create_method_call_event` converts it to JSON for the model, and `Drop` recurses).
493/// Without this cap a 4 MiB body is a value ~100 000 deep, and 16 000 levels already overflow
494/// an 8 MiB stack: `fuzz/fuzz_targets/xmlrpc_value.rs`'s `depth_bomb` seed `SIGSEGV`s with the
495/// check disabled. It also bounds the parser's own container stack.
496#[cfg(not(target_arch = "wasm32"))]
497pub const MAX_VALUE_DEPTH: usize = 64;
498
499/// A container currently being filled.
500#[cfg(not(target_arch = "wasm32"))]
501enum Container {
502    Array(Vec<XmlRpcValue>),
503    Struct {
504        members: Vec<(String, XmlRpcValue)>,
505        pending_name: Option<String>,
506    },
507}
508
509/// Parse an XML-RPC `<methodCall>`.
510///
511/// The previous implementation pushed `XmlRpcValue::String(text)` for every text
512/// node and never looked at the type element, so `<int>5</int>` reached the model
513/// as `"5"` and six of the ten `XmlRpcValue` variants were unreachable. It also
514/// collected array elements only at `</data>` (keeping just the last one and
515/// leaking the rest into the next parameter), dropped parameters whose value was
516/// empty (shifting every later parameter down one position), and returned
517/// `Ok(MethodCall { method_name: "", .. })` for input that was not XML-RPC at all.
518///
519/// This version tracks the active type element, closes each value at `</value>`,
520/// and validates the document shape.
521///
522/// Public for `fuzz/fuzz_targets/xmlrpc_value.rs`, which hands whatever this accepts to
523/// `actions::create_method_call_event` exactly as the request path does.
524#[cfg(not(target_arch = "wasm32"))]
525pub fn parse_method_call(xml: &str) -> Result<MethodCall> {
526    let mut reader = Reader::from_str(xml);
527    reader.config_mut().trim_text(true);
528
529    let mut method_name: Option<String> = None;
530    let mut params: Vec<XmlRpcValue> = Vec::new();
531    let mut buf = Vec::new();
532
533    let mut saw_method_call = false;
534    let mut method_call_closed = false;
535    let mut in_method_name = false;
536    let mut in_member_name = false;
537
538    // One frame per open <value>; the frame holds the value once its type element
539    // has been closed.
540    let mut value_frames: Vec<Option<XmlRpcValue>> = Vec::new();
541    let mut containers: Vec<Container> = Vec::new();
542    // The type element currently open inside the innermost <value>, if any.
543    let mut current_type: Option<Vec<u8>> = None;
544    let mut current_param: Option<XmlRpcValue> = None;
545
546    /// Attach a completed value to the innermost open `<value>`.
547    fn set_frame(frames: &mut [Option<XmlRpcValue>], value: XmlRpcValue) {
548        if let Some(frame) = frames.last_mut() {
549            *frame = Some(value);
550        }
551    }
552
553    /// A closed `<value>` belongs to the enclosing container, or to the parameter.
554    fn emit(
555        containers: &mut [Container],
556        current_param: &mut Option<XmlRpcValue>,
557        value: XmlRpcValue,
558    ) {
559        match containers.last_mut() {
560            Some(Container::Array(items)) => items.push(value),
561            Some(Container::Struct {
562                members,
563                pending_name,
564            }) => {
565                let name = pending_name.take().unwrap_or_default();
566                members.push((name, value));
567            }
568            None => *current_param = Some(value),
569        }
570    }
571
572    loop {
573        match reader.read_event_into(&mut buf) {
574            Ok(XmlEvent::Start(ref e)) => {
575                let name = e.name().as_ref().to_vec();
576                match name.as_slice() {
577                    b"methodCall" => saw_method_call = true,
578                    b"methodName" => in_method_name = true,
579                    b"param" => current_param = None,
580                    // The depth guard is on every nesting element, not only `<value>`.
581                    // `<array>` and `<struct>` used to push a container with no check at all,
582                    // and a well-formed document may open one without an enclosing `<value>`:
583                    // `<array><array><array>…` is 7 bytes a level, so a body at the 4 MiB cap
584                    // pushed ~600 000 frames. Bounded by the body cap rather than unbounded,
585                    // but it is allocation a peer chooses and nothing needed.
586                    b"value" | b"array" | b"struct" => {
587                        if value_frames.len() + containers.len() >= MAX_VALUE_DEPTH {
588                            return Err(anyhow::anyhow!(
589                                "value nesting deeper than {} levels",
590                                MAX_VALUE_DEPTH
591                            ));
592                        }
593                        match name.as_slice() {
594                            b"value" => {
595                                value_frames.push(None);
596                                current_type = None;
597                            }
598                            b"array" => containers.push(Container::Array(Vec::new())),
599                            _ => containers.push(Container::Struct {
600                                members: Vec::new(),
601                                pending_name: None,
602                            }),
603                        }
604                    }
605                    b"name" => in_member_name = true,
606                    b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
607                    | b"dateTime.iso8601" | b"base64" | b"nil" => {
608                        current_type = Some(name);
609                    }
610                    _ => {}
611                }
612            }
613            Ok(XmlEvent::End(ref e)) => match e.name().as_ref() {
614                b"methodCall" => method_call_closed = true,
615                b"methodName" => in_method_name = false,
616                b"name" => in_member_name = false,
617                b"value" => {
618                    // An empty <value/> or <value></value> is the empty string
619                    // (spec: "If no type is indicated, the type is string").
620                    let value = value_frames
621                        .pop()
622                        .flatten()
623                        .unwrap_or_else(|| XmlRpcValue::String(String::new()));
624                    emit(&mut containers, &mut current_param, value);
625                    current_type = None;
626                }
627                b"param" => {
628                    params.push(
629                        current_param
630                            .take()
631                            .unwrap_or_else(|| XmlRpcValue::String(String::new())),
632                    );
633                }
634                b"array" => {
635                    if let Some(Container::Array(items)) = containers.pop() {
636                        set_frame(&mut value_frames, XmlRpcValue::Array(items));
637                    }
638                }
639                b"struct" => {
640                    if let Some(Container::Struct { members, .. }) = containers.pop() {
641                        set_frame(&mut value_frames, XmlRpcValue::Struct(members));
642                    }
643                }
644                tag @ (b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
645                | b"dateTime.iso8601" | b"base64" | b"nil") => {
646                    // No text node arrived, e.g. <string></string> or <int></int>.
647                    if value_frames.last().map(|f| f.is_none()).unwrap_or(false) {
648                        set_frame(&mut value_frames, empty_typed_value(tag));
649                    }
650                    current_type = None;
651                }
652                _ => {}
653            },
654            Ok(XmlEvent::Text(e)) => {
655                let text = e.unescape()?.to_string();
656                if in_method_name {
657                    method_name = Some(text);
658                } else if in_member_name {
659                    if let Some(Container::Struct { pending_name, .. }) = containers.last_mut() {
660                        *pending_name = Some(text);
661                    }
662                } else if !value_frames.is_empty() {
663                    let value = typed_value(current_type.as_deref(), &text)?;
664                    set_frame(&mut value_frames, value);
665                }
666            }
667            Ok(XmlEvent::Empty(ref e)) => {
668                // Self-closing type element: <nil/>, <string/>, <int/>.
669                let tag = e.name().as_ref().to_vec();
670                if !value_frames.is_empty() {
671                    match tag.as_slice() {
672                        b"i4" | b"int" | b"i8" | b"boolean" | b"string" | b"double"
673                        | b"dateTime.iso8601" | b"base64" | b"nil" => {
674                            set_frame(&mut value_frames, empty_typed_value(&tag));
675                        }
676                        b"value" => {
677                            emit(
678                                &mut containers,
679                                &mut current_param,
680                                XmlRpcValue::String(String::new()),
681                            );
682                        }
683                        _ => {}
684                    }
685                } else if tag.as_slice() == b"value" {
686                    emit(
687                        &mut containers,
688                        &mut current_param,
689                        XmlRpcValue::String(String::new()),
690                    );
691                }
692            }
693            Ok(XmlEvent::Eof) => break,
694            Err(e) => return Err(anyhow::anyhow!("XML parse error: {}", e)),
695            _ => {}
696        }
697        buf.clear();
698    }
699
700    // Reject anything that is not a well-formed methodCall rather than reporting a
701    // call to the empty method name. A browser GET, a JSON body or a truncated
702    // document all used to arrive at the model as method "" with no parameters.
703    if !saw_method_call {
704        return Err(anyhow::anyhow!("not an XML-RPC methodCall document"));
705    }
706    // quick-xml reports EOF rather than an error when elements are still open, so
707    // a body truncated mid-document used to be handled as a complete call.
708    if !method_call_closed {
709        return Err(anyhow::anyhow!("truncated methodCall document"));
710    }
711    let method_name = match method_name {
712        Some(name) if !name.is_empty() => name,
713        _ => return Err(anyhow::anyhow!("methodCall has no methodName")),
714    };
715    if !value_frames.is_empty() || !containers.is_empty() {
716        return Err(anyhow::anyhow!("unterminated value, array or struct"));
717    }
718
719    Ok(MethodCall {
720        method_name,
721        params,
722    })
723}
724
725/// Decode a text node according to the type element that encloses it.
726#[cfg(not(target_arch = "wasm32"))]
727fn typed_value(tag: Option<&[u8]>, text: &str) -> Result<XmlRpcValue> {
728    let parse_err = |ty: &str| anyhow::anyhow!("invalid <{}> value: {:?}", ty, text);
729    Ok(match tag {
730        // Untyped text inside <value> is a string, per the specification.
731        None | Some(b"string") => XmlRpcValue::String(text.to_string()),
732        Some(b"i4") | Some(b"int") => {
733            XmlRpcValue::Int(text.trim().parse::<i32>().map_err(|_| parse_err("int"))?)
734        }
735        Some(b"i8") => XmlRpcValue::I8(text.trim().parse::<i64>().map_err(|_| parse_err("i8"))?),
736        Some(b"boolean") => match text.trim() {
737            "1" | "true" => XmlRpcValue::Boolean(true),
738            "0" | "false" => XmlRpcValue::Boolean(false),
739            _ => return Err(parse_err("boolean")),
740        },
741        Some(b"double") => {
742            let d = text
743                .trim()
744                .parse::<f64>()
745                .map_err(|_| parse_err("double"))?;
746            if !d.is_finite() {
747                return Err(parse_err("double"));
748            }
749            XmlRpcValue::Double(d)
750        }
751        Some(b"dateTime.iso8601") => XmlRpcValue::DateTime(text.trim().to_string()),
752        Some(b"base64") => {
753            use base64::Engine;
754            XmlRpcValue::Base64(
755                base64::engine::general_purpose::STANDARD
756                    .decode(text.trim())
757                    .map_err(|_| parse_err("base64"))?,
758            )
759        }
760        Some(b"nil") => XmlRpcValue::Nil,
761        Some(_) => XmlRpcValue::String(text.to_string()),
762    })
763}
764
765/// The value of a type element that contains no text, e.g. `<string/>`.
766#[cfg(not(target_arch = "wasm32"))]
767fn empty_typed_value(tag: &[u8]) -> XmlRpcValue {
768    match tag {
769        b"i4" | b"int" => XmlRpcValue::Int(0),
770        b"i8" => XmlRpcValue::I8(0),
771        b"boolean" => XmlRpcValue::Boolean(false),
772        b"double" => XmlRpcValue::Double(0.0),
773        b"base64" => XmlRpcValue::Base64(Vec::new()),
774        b"nil" => XmlRpcValue::Nil,
775        _ => XmlRpcValue::String(String::new()),
776    }
777}
778
779/// Generate an XML-RPC fault response.
780///
781/// The message is XML-escaped. It used to be interpolated raw with `format!`, so a
782/// fault_string from the model containing `<`, `>` or `&` — "Unknown method: <foo>"
783/// is entirely plausible — produced a document no client could parse. The same
784/// applied to the parse-error and internal-error paths, which embed the text of a
785/// `serde`/`quick-xml` error.
786#[cfg(not(target_arch = "wasm32"))]
787pub fn generate_fault(code: i32, message: &str) -> String {
788    format!(
789        r#"<?xml version="1.0"?>
790<methodResponse>
791  <fault>
792    <value>
793      <struct>
794        <member>
795          <name>faultCode</name>
796          <value><int>{}</int></value>
797        </member>
798        <member>
799          <name>faultString</name>
800          <value><string>{}</string></value>
801        </member>
802      </struct>
803    </value>
804  </fault>
805</methodResponse>"#,
806        code,
807        escape_xml_text(message)
808    )
809}
810
811/// Escape the five XML predefined entities in character data.
812#[cfg(not(target_arch = "wasm32"))]
813fn escape_xml_text(text: &str) -> String {
814    let mut out = String::with_capacity(text.len());
815    for c in text.chars() {
816        match c {
817            '&' => out.push_str("&amp;"),
818            '<' => out.push_str("&lt;"),
819            '>' => out.push_str("&gt;"),
820            '"' => out.push_str("&quot;"),
821            '\'' => out.push_str("&apos;"),
822            _ => out.push(c),
823        }
824    }
825    out
826}
827
828/// Generate XML-RPC success response with a value
829#[cfg(not(target_arch = "wasm32"))]
830pub fn generate_success_response(value: &XmlRpcValue) -> String {
831    let mut writer = Writer::new(Cursor::new(Vec::new()));
832
833    // XML declaration
834    writer
835        .write_event(XmlEvent::Decl(quick_xml::events::BytesDecl::new(
836            "1.0", None, None,
837        )))
838        .unwrap();
839
840    // methodResponse
841    writer
842        .write_event(XmlEvent::Start(BytesStart::new("methodResponse")))
843        .unwrap();
844    writer
845        .write_event(XmlEvent::Start(BytesStart::new("params")))
846        .unwrap();
847    writer
848        .write_event(XmlEvent::Start(BytesStart::new("param")))
849        .unwrap();
850
851    write_value(&mut writer, value);
852
853    writer
854        .write_event(XmlEvent::End(BytesEnd::new("param")))
855        .unwrap();
856    writer
857        .write_event(XmlEvent::End(BytesEnd::new("params")))
858        .unwrap();
859    writer
860        .write_event(XmlEvent::End(BytesEnd::new("methodResponse")))
861        .unwrap();
862
863    String::from_utf8(writer.into_inner().into_inner()).unwrap()
864}
865
866/// Write XML-RPC value to XML writer
867#[cfg(not(target_arch = "wasm32"))]
868fn write_value(writer: &mut Writer<Cursor<Vec<u8>>>, value: &XmlRpcValue) {
869    writer
870        .write_event(XmlEvent::Start(BytesStart::new("value")))
871        .unwrap();
872
873    match value {
874        XmlRpcValue::Int(i) => {
875            writer
876                .write_event(XmlEvent::Start(BytesStart::new("int")))
877                .unwrap();
878            writer
879                .write_event(XmlEvent::Text(BytesText::new(&i.to_string())))
880                .unwrap();
881            writer
882                .write_event(XmlEvent::End(BytesEnd::new("int")))
883                .unwrap();
884        }
885        XmlRpcValue::I8(i) => {
886            writer
887                .write_event(XmlEvent::Start(BytesStart::new("i8")))
888                .unwrap();
889            writer
890                .write_event(XmlEvent::Text(BytesText::new(&i.to_string())))
891                .unwrap();
892            writer
893                .write_event(XmlEvent::End(BytesEnd::new("i8")))
894                .unwrap();
895        }
896        XmlRpcValue::Boolean(b) => {
897            writer
898                .write_event(XmlEvent::Start(BytesStart::new("boolean")))
899                .unwrap();
900            writer
901                .write_event(XmlEvent::Text(BytesText::new(if *b { "1" } else { "0" })))
902                .unwrap();
903            writer
904                .write_event(XmlEvent::End(BytesEnd::new("boolean")))
905                .unwrap();
906        }
907        XmlRpcValue::String(s) => {
908            writer
909                .write_event(XmlEvent::Start(BytesStart::new("string")))
910                .unwrap();
911            writer
912                .write_event(XmlEvent::Text(BytesText::new(s)))
913                .unwrap();
914            writer
915                .write_event(XmlEvent::End(BytesEnd::new("string")))
916                .unwrap();
917        }
918        XmlRpcValue::Double(d) => {
919            writer
920                .write_event(XmlEvent::Start(BytesStart::new("double")))
921                .unwrap();
922            writer
923                .write_event(XmlEvent::Text(BytesText::new(&d.to_string())))
924                .unwrap();
925            writer
926                .write_event(XmlEvent::End(BytesEnd::new("double")))
927                .unwrap();
928        }
929        XmlRpcValue::DateTime(dt) => {
930            writer
931                .write_event(XmlEvent::Start(BytesStart::new("dateTime.iso8601")))
932                .unwrap();
933            writer
934                .write_event(XmlEvent::Text(BytesText::new(dt)))
935                .unwrap();
936            writer
937                .write_event(XmlEvent::End(BytesEnd::new("dateTime.iso8601")))
938                .unwrap();
939        }
940        XmlRpcValue::Base64(bytes) => {
941            use base64::Engine;
942            let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
943            writer
944                .write_event(XmlEvent::Start(BytesStart::new("base64")))
945                .unwrap();
946            writer
947                .write_event(XmlEvent::Text(BytesText::new(&encoded)))
948                .unwrap();
949            writer
950                .write_event(XmlEvent::End(BytesEnd::new("base64")))
951                .unwrap();
952        }
953        XmlRpcValue::Array(arr) => {
954            writer
955                .write_event(XmlEvent::Start(BytesStart::new("array")))
956                .unwrap();
957            writer
958                .write_event(XmlEvent::Start(BytesStart::new("data")))
959                .unwrap();
960            for item in arr {
961                write_value(writer, item);
962            }
963            writer
964                .write_event(XmlEvent::End(BytesEnd::new("data")))
965                .unwrap();
966            writer
967                .write_event(XmlEvent::End(BytesEnd::new("array")))
968                .unwrap();
969        }
970        XmlRpcValue::Struct(members) => {
971            writer
972                .write_event(XmlEvent::Start(BytesStart::new("struct")))
973                .unwrap();
974            for (name, val) in members {
975                writer
976                    .write_event(XmlEvent::Start(BytesStart::new("member")))
977                    .unwrap();
978                writer
979                    .write_event(XmlEvent::Start(BytesStart::new("name")))
980                    .unwrap();
981                writer
982                    .write_event(XmlEvent::Text(BytesText::new(name)))
983                    .unwrap();
984                writer
985                    .write_event(XmlEvent::End(BytesEnd::new("name")))
986                    .unwrap();
987                write_value(writer, val);
988                writer
989                    .write_event(XmlEvent::End(BytesEnd::new("member")))
990                    .unwrap();
991            }
992            writer
993                .write_event(XmlEvent::End(BytesEnd::new("struct")))
994                .unwrap();
995        }
996        XmlRpcValue::Nil => {
997            writer
998                .write_event(XmlEvent::Empty(BytesStart::new("nil")))
999                .unwrap();
1000        }
1001    }
1002
1003    writer
1004        .write_event(XmlEvent::End(BytesEnd::new("value")))
1005        .unwrap();
1006}