Skip to main content

netget/server/saml_sp/
mod.rs

1//! SAML Service Provider (SP) server implementation
2//!
3//! Serves the SP side of SAML 2.0 Web Browser SSO: `/login` starts SP-initiated SSO, `/acs`
4//! receives the IDP's SAMLResponse, `/metadata` describes the SP. The model reads the
5//! assertion and decides who the user is.
6//!
7//! **NetGet verifies nothing.** The SAMLResponse body is passed to the model as text; no XML
8//! signature is checked (there is no key to check it against), and neither are issuer,
9//! audience, expiry or assertion-ID replay. This is a simulator for exercising IDPs and for
10//! honeypots — never an access-control boundary.
11
12pub mod actions;
13
14use std::convert::Infallible;
15use std::net::SocketAddr;
16use std::sync::Arc;
17
18use bytes::Bytes;
19use http_body_util::{BodyExt, Full};
20use hyper::body::{Body, Incoming};
21use hyper::server::conn::http1;
22use hyper::service::service_fn;
23use hyper::{Request, Response, StatusCode};
24use hyper_util::rt::TokioIo;
25use tokio::io::AsyncWriteExt;
26use tokio::sync::mpsc;
27use tracing::{debug, error, trace, warn};
28
29use crate::llm::action_helper::call_llm;
30use crate::llm::ollama_client::OllamaClient;
31use crate::logging::emit::Log;
32use crate::protocol::Event;
33use crate::server::connection::ConnectionId;
34use crate::server::SamlSpProtocol;
35use crate::state::app_state::AppState;
36use actions::SAML_SP_REQUEST_EVENT;
37
38/// Largest request body this server will buffer.
39///
40/// `/acs`, `/sso` and every other route here is reachable with no credential at all, and the
41/// body is handed to the model verbatim as prompt text, so the previous unbounded
42/// `req.collect()` let one anonymous POST grow the process without limit and drive an LLM
43/// call with megabytes of attacker-chosen prompt. A base64 SAMLResponse form is a few tens of
44/// KiB; 256 KiB is generous for even a large assertion with attributes.
45pub const MAX_REQUEST_BYTES: usize = 256 * 1024;
46
47/// How long a peer that has connected and produced nothing may hold a slot.
48///
49/// HTTP is client-speaks-first: the server says nothing until a request line arrives, so a peer
50/// that has completed the TCP handshake and sent no byte has asked nothing and negotiated
51/// nothing. That state carries no protocol yet, which is why this number is the same across
52/// netget's HTTP-shaped servers while the idle bound below is not. Apache's `mod_reqtimeout`
53/// gives the request header 20s and nginx's `client_header_timeout` 60s; 30s sits between the
54/// two deployed norms.
55///
56/// hyper's own `header_read_timeout` is **not** this bound: its 30-second default is inert
57/// unless `http1::Builder::timer` is also set, which nothing here does — hyper downgrades a
58/// defaulted duration to `None` when no timer is present and applies no deadline at all.
59const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
60
61/// How long a connection with nothing outstanding may sit idle between requests.
62///
63/// These endpoints are reached two ways and the two pull in opposite directions. A **browser**
64/// does one redirect round and never comes back on that connection, which is the case Apache's
65/// `KeepAliveTimeout` default of 5s is tuned for. A **relying party's back-channel** client
66/// (token exchange, introspection, a JWKS or metadata fetch) reuses a pooled connection, which
67/// is the case nginx's 75-second `keepalive_timeout` is tuned for. 60s is comfortably past any
68/// pooled back-channel round trip while not letting a browser that navigated away hold a slot
69/// for minutes. Nothing here streams or long-polls, so no legitimate request sits open.
70///
71/// **The five-minute numbers SAML and OIDC do quote are not this number.** An assertion's
72/// `NotOnOrAfter` and an `id_token`'s `exp` bound how long a *credential* may be presented, not
73/// how long a socket may be silent; copying one into the other is the mistake this comment
74/// exists to prevent. And a request still being answered is not silence either: the watchdog
75/// reads [`ConnectionActivity`](crate::server::accept_bounded::ConnectionActivity), which
76/// reports a connection with work in flight — a model round-trip, or an event a `manual` rule
77/// parked for a human — as not idle at all.
78const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
79
80/// Concurrent connections this server admits.
81///
82/// The shared default. Each admitted connection may buffer one body of up to
83/// [`MAX_REQUEST_BYTES`] (256 KiB), so the cap is what turns that per-connection bound into a total
84/// one and holds the worst case well inside the ~1 GiB ceiling netget's HTTP family is held
85/// to. A protocol only declares a smaller number when its per-connection cost is larger.
86const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
87
88/// What a peer over [`MAX_CONNECTIONS`] is told before the socket closes.
89///
90/// `503 Service Unavailable` with a `Retry-After`, written directly onto the socket because the
91/// peer has not sent a request line for hyper to answer. Fixed bytes: nothing derived from an
92/// error reaches the wire (see `crate::utils::wire_failure`).
93const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
94    Content-Length: 0\r\nRetry-After: 5\r\nConnection: close\r\n\r\n";
95
96/// Narrow a model-supplied HTTP status to `u16` without wrapping.
97///
98/// `status as u16` on a `u64` truncates, and the truncation is the dangerous direction here:
99/// `65736` becomes `200`, and a 2xx is the only thing an SP treats as a completed sign-in. A
100/// status the model cannot have meant must not become the one that admits someone.
101fn status_or(value: Option<&serde_json::Value>, default: u16) -> u16 {
102    match value.and_then(|v| v.as_u64()) {
103        Some(raw) => u16::try_from(raw)
104            .ok()
105            .filter(|s| (100..=599).contains(s))
106            .unwrap_or_else(|| {
107                warn!("SAML SP: ignoring out-of-range status {raw}, using {default}");
108                default
109            }),
110        None => default,
111    }
112}
113
114/// Build a response from parts that came from the model, without ever panicking.
115///
116/// `status` and every response header are model output (`send_error_response` documents
117/// `status_code`, and `process_assertion` puts a model-supplied user id into `Set-Cookie`).
118/// The previous code did `Response::builder().status(status as u16)…body(..).unwrap()`, so a
119/// `status_code` of 1000 — or a header value containing CR/LF — panicked inside the
120/// connection task. Local copy of `http_common::handler::build_safe_response`, which the
121/// `saml-sp` feature cannot reach because `http_common` is gated on `feature = "http"`.
122fn build_safe_response(
123    status: u16,
124    headers: impl IntoIterator<Item = (String, String)>,
125    body: String,
126) -> Response<Full<Bytes>> {
127    let status_code = StatusCode::from_u16(status).unwrap_or_else(|_| {
128        error!("SAML SP: invalid HTTP status {status}, sending 500 instead");
129        StatusCode::INTERNAL_SERVER_ERROR
130    });
131
132    let mut builder = Response::builder().status(status_code);
133    for (name, value) in headers {
134        match (
135            hyper::header::HeaderName::from_bytes(name.as_bytes()),
136            hyper::header::HeaderValue::from_str(&value),
137        ) {
138            (Ok(n), Ok(v)) => builder = builder.header(n, v),
139            _ => warn!("SAML SP: dropping invalid response header {name:?}"),
140        }
141    }
142
143    builder
144        .body(Full::new(Bytes::from(body)))
145        .unwrap_or_else(|e| {
146            error!("SAML SP: failed to build response ({e}), sending bare 500");
147            let mut fallback =
148                Response::new(Full::new(Bytes::from_static(b"Internal Server Error")));
149            *fallback.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
150            fallback
151        })
152}
153
154/// SAML SP server that delegates authentication and assertion generation to LLM
155pub struct SamlSpServer;
156
157impl SamlSpServer {
158    /// Spawn the SAML SP server with LLM-controlled authentication
159    pub async fn spawn_with_llm_actions(
160        listen_addr: SocketAddr,
161        llm_client: OllamaClient,
162        app_state: Arc<AppState>,
163        status_tx: mpsc::UnboundedSender<String>,
164        server_id: crate::state::ServerId,
165    ) -> anyhow::Result<SocketAddr> {
166        let listener =
167            crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
168        let local_addr = listener.local_addr()?;
169
170        Log::new(Some(&status_tx)).info(format!("SAML SP server listening on {}", local_addr));
171
172        let protocol = Arc::new(SamlSpProtocol::new());
173
174        // Spawn server loop
175        let task_registrar = app_state.clone();
176        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
177        let accept_handle = tokio::spawn(async move {
178            loop {
179                match crate::server::accept_bounded::accept_bounded(
180                    &listener,
181                    &limiter,
182                    CONNECTION_CAP_REFUSAL,
183                    "SAML SP",
184                    Some(&status_tx),
185                )
186                .await
187                {
188                    Ok((stream, remote_addr, permit)) => {
189                        let connection_id =
190                            ConnectionId::new(app_state.get_next_unified_id().await);
191                        let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
192
193                        Log::new(Some(&status_tx)).info(format!(
194                            "Accepted SAML SP connection {} from {}",
195                            connection_id, remote_addr
196                        ));
197
198                        let status_tx_for_task = status_tx.clone();
199
200                        // Add connection to ServerInstance
201                        use crate::state::server::{
202                            ConnectionState as ServerConnectionState, ConnectionStatus,
203                            ProtocolConnectionInfo,
204                        };
205                        let now = crate::utils::clock::Instant::now();
206                        let conn_state = ServerConnectionState {
207                            id: connection_id,
208                            remote_addr,
209                            local_addr: local_addr_conn,
210                            bytes_sent: 0,
211                            bytes_received: 0,
212                            packets_sent: 0,
213                            packets_received: 0,
214                            last_activity: now,
215                            status: ConnectionStatus::Active,
216                            status_changed_at: now,
217                            protocol_info: ProtocolConnectionInfo::empty(),
218                        };
219                        app_state
220                            .add_connection_to_server(server_id, conn_state)
221                            .await;
222                        let _ = status_tx.send("__UPDATE_UI__".to_string());
223
224                        let llm_client_clone = llm_client.clone();
225                        let app_state_clone = app_state.clone();
226                        let protocol_clone = protocol.clone();
227
228                        // Spawn a task to handle this connection
229                        // Tracked, not detached: stop_server must abort this task too.
230                        let task_owner = app_state.clone();
231                        task_owner
232                            .spawn_server_task(server_id, async move {
233                                let status_tx = status_tx_for_task;
234                                // Held for the life of the connection, so the cap counts live
235                                // clients rather than accepts.
236                                let _permit = permit;
237
238                                // First-byte bound, before hyper sees the socket.
239                                //
240                                // hyper owns every read once `serve_connection` starts, and a
241                                // deadline on those reads would be wrong here rather than merely
242                                // awkward: hyper keeps polling the connection for more input
243                                // while a request is being answered, so such a deadline would
244                                // fire in the middle of a model round-trip. `peek` waits for data
245                                // without consuming it, so the request line is still there for
246                                // hyper afterwards, and it bounds exactly the case that needs
247                                // bounding - a peer that has connected and sent nothing at all.
248                                let spoke = matches!(
249                                    tokio::time::timeout(
250                                        FIRST_BYTE_READ_TIMEOUT,
251                                        stream.peek(&mut [0u8; 1]),
252                                    )
253                                    .await,
254                                    Ok(Ok(n)) if n > 0
255                                );
256
257                                let io = TokioIo::new(stream);
258
259                                // Clone for service_fn closure
260                                let llm_for_service = llm_client_clone.clone();
261                                let state_for_service = app_state_clone.clone();
262                                let status_for_service = status_tx.clone();
263                                let protocol_for_service = protocol_clone.clone();
264
265                                // Tracks whether this connection is answering anything. A
266                                // request waiting on the model, or parked for a human by a
267                                // `manual` rule, holds the count above zero, so the idle watchdog
268                                // below cannot close the connection the answer belongs to however
269                                // long it takes - only genuine silence counts.
270                                let activity = std::sync::Arc::new(
271                                    crate::server::accept_bounded::ConnectionActivity::new(),
272                                );
273                                let activity_for_service = std::sync::Arc::clone(&activity);
274
275                                // Create a service that handles SAML SP requests with LLM
276                                let service = service_fn(move |req: Request<Incoming>| {
277                                    let llm_clone = llm_for_service.clone();
278                                    let state_clone = state_for_service.clone();
279                                    let status_clone = status_for_service.clone();
280                                    let protocol_clone = protocol_for_service.clone();
281                                    let activity = std::sync::Arc::clone(&activity_for_service);
282                                    async move {
283                                        let _busy = activity.busy();
284                                        handle_saml_sp_request(
285                                            req,
286                                            connection_id,
287                                            server_id,
288                                            remote_addr,
289                                            llm_clone,
290                                            state_clone,
291                                            status_clone,
292                                            protocol_clone,
293                                        )
294                                        .await
295                                    }
296                                });
297
298                                // Serve HTTP/1 on this connection, bounded at both ends.
299                                if !spoke {
300                                    Log::new(Some(&status_tx)).debug(format!(
301                                        "SAML SP peer {} sent nothing for {}s; closing before \
302                                         any request",
303                                        remote_addr,
304                                        FIRST_BYTE_READ_TIMEOUT.as_secs()
305                                    ));
306                                } else {
307                                    // Keep the socket after hyper flushes the response. An oversized
308                                    // upload can still be sending when the 413 is ready.
309                                    let conn = http1::Builder::new()
310                                        .serve_connection(io, service)
311                                        .without_shutdown();
312                                    tokio::pin!(conn);
313                                    tokio::select! {
314                                        result = &mut conn => {
315                                            match result {
316                                                Ok(parts) => {
317                                                    let mut stream = parts.io.into_inner();
318                                                    // Send EOF after the response, then discard
319                                                    // the upload tail within shared bounds.
320                                                    let _ = stream.shutdown().await;
321                                                    crate::server::accept_bounded::drain_after_response(&mut stream).await;
322                                                }
323                                                Err(err) => error!(
324                                                    "Error serving SAML SP connection \
325                                                     {}: {}",
326                                                    connection_id,
327                                                    err
328                                                ),
329                                            }
330                                        }
331                                        _ = crate::server::accept_bounded::watch_idle(
332                                            std::sync::Arc::clone(&activity),
333                                            IDLE_BETWEEN_REQUESTS_TIMEOUT,
334                                        ) => {
335                                            Log::new(Some(&status_tx)).debug(format!(
336                                                "SAML SP connection {} idle for {}s; closing",
337                                                connection_id,
338                                                IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
339                                            ));
340                                        }
341                                    }
342                                }
343
344                                // Remove connection when done
345                                debug!("SAML SP connection {} closed", connection_id);
346                                app_state_clone
347                                    .remove_connection_from_server(server_id, connection_id)
348                                    .await;
349                                let _ = status_tx.send("__UPDATE_UI__".to_string());
350                            })
351                            .await;
352                    }
353                    Err(e) => {
354                        Log::new(Some(&status_tx))
355                            .error(format!("Failed to accept SAML SP connection: {}", e));
356                    }
357                }
358            }
359        });
360
361        // Register the accept loop so stop_server can abort it and release the port.
362        task_registrar
363            .register_server_task(server_id, accept_handle)
364            .await;
365
366        Ok(local_addr)
367    }
368}
369
370/// Handle a SAML SP request with LLM decision making
371async fn handle_saml_sp_request(
372    req: Request<Incoming>,
373    connection_id: ConnectionId,
374    server_id: crate::state::ServerId,
375    remote_addr: SocketAddr,
376    llm_client: OllamaClient,
377    app_state: Arc<AppState>,
378    status_tx: mpsc::UnboundedSender<String>,
379    protocol: Arc<SamlSpProtocol>,
380) -> Result<Response<Full<Bytes>>, Infallible> {
381    let method = req.method().clone();
382    let path = req.uri().path().to_string();
383    let query = req.uri().query().map(|q| q.to_string());
384
385    Log::new(Some(&status_tx)).debug(format!("SAML SP {} {} from {}", method, path, remote_addr));
386
387    // Extract headers
388    let headers: Vec<(String, String)> = req
389        .headers()
390        .iter()
391        .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
392        .collect();
393
394    // Read request body
395    // Bounded, and refused rather than truncated: a cut-off SAMLResponse would reach the
396    // model as a well-formed request whose assertion happened to end early.
397    let body_bytes = match http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BYTES)
398        .collect()
399        .await
400    {
401        Ok(collected) => collected.to_bytes().to_vec(),
402        Err(e) => {
403            Log::new(Some(&status_tx)).warn(format!(
404                "SAML SP {} {} decision=fail_closed_body_rejected (limit {} bytes): {}",
405                method, path, MAX_REQUEST_BYTES, e
406            ));
407            return Ok(build_safe_response(
408                413,
409                [(
410                    "content-type".to_string(),
411                    "text/plain; charset=utf-8".to_string(),
412                )],
413                "request body too large".to_string(),
414            ));
415        }
416    };
417
418    // TRACE level for full payloads
419    if !body_bytes.is_empty() {
420        trace!("SAML SP request body: {} bytes", body_bytes.len());
421        if let Ok(body_str) = String::from_utf8(body_bytes.clone()) {
422            trace!("SAML SP request body content: {}", body_str);
423        }
424    }
425
426    // Update connection stats
427    app_state
428        .update_connection_stats(
429            server_id,
430            connection_id,
431            Some(body_bytes.len() as u64),
432            None,
433            None,
434            None,
435        )
436        .await;
437
438    // Build event for LLM
439    let event = Event::new(
440        &SAML_SP_REQUEST_EVENT,
441        serde_json::json!({
442            "method": method.to_string(),
443            "path": path.clone(),
444            "query": query,
445            "headers": headers,
446            "body": if body_bytes.is_empty() {
447                serde_json::Value::Null
448            } else if let Ok(body_str) = String::from_utf8(body_bytes.clone()) {
449                serde_json::Value::String(body_str)
450            } else {
451                // Never base64 in event data: a model cannot decode it, and this body is
452                // always text on a working SAML binding (the assertion's own base64 arrives
453                // inside a urlencoded form field). Say what happened instead.
454                serde_json::Value::String(format!(
455                    "<{} bytes of non-UTF-8 data; not a SAML binding this server understands>",
456                    body_bytes.len()
457                ))
458            },
459            "client_ip": remote_addr.ip().to_string(),
460        }),
461    );
462
463    // Call LLM for decision
464    debug!("Calling LLM for SAML SP request decision");
465    let action_result = call_llm(
466        &llm_client,
467        &app_state,
468        server_id,
469        Some(connection_id),
470        &event,
471        protocol.as_ref(),
472    )
473    .await;
474
475    // Execute actions and build response
476    let response = match action_result {
477        Ok(result) => {
478            // Three outcomes have to stay distinguishable in the log, because they mean very
479            // different things to an operator: the model refused (`decision=model_reject`),
480            // the model said nothing usable (`decision=fail_closed_no_answer`), and the
481            // backend erred (`decision=fail_closed_llm_error`, in the `Err` arm below).
482            //
483            // The default status used to be 200: a model that answered only with a
484            // common/memory action, or whose protocol result was not a parseable
485            // `Output`, produced an empty `200 OK`. For an SP that is a permissive
486            // default on a no-answer path — it is not a sign-in (no `Set-Cookie`), but it
487            // tells the browser the request succeeded. There is no default now; a
488            // response is emitted only if a usable `Output` was actually seen.
489            use crate::llm::actions::protocol_trait::ActionResult;
490
491            /// Flatten `Multiple` so an `Output` wrapped in one is not silently dropped.
492            fn collect_outputs(result: ActionResult, out: &mut Vec<Vec<u8>>) {
493                match result {
494                    ActionResult::Output(data) => out.push(data),
495                    ActionResult::Multiple(inner) => {
496                        for r in inner {
497                            collect_outputs(r, out);
498                        }
499                    }
500                    _ => {}
501                }
502            }
503
504            let mut outputs = Vec::new();
505            for protocol_result in result.protocol_results {
506                collect_outputs(protocol_result, &mut outputs);
507            }
508
509            let mut answer: Option<(u16, std::collections::HashMap<String, String>, String)> = None;
510            for output_data in outputs {
511                let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(&output_data)
512                else {
513                    warn!(
514                        "SAML SP: ignoring non-JSON action output ({} bytes)",
515                        output_data.len()
516                    );
517                    continue;
518                };
519                let (status_code, response_headers, response_body) =
520                    answer.get_or_insert_with(|| {
521                        (200u16, std::collections::HashMap::new(), String::new())
522                    });
523                if json_value.get("status").is_some() {
524                    *status_code = status_or(json_value.get("status"), 200);
525                }
526                if let Some(headers_obj) = json_value.get("headers").and_then(|v| v.as_object()) {
527                    for (k, v) in headers_obj {
528                        if let Some(v_str) = v.as_str() {
529                            response_headers.insert(k.clone(), v_str.to_string());
530                        }
531                    }
532                }
533                if let Some(body) = json_value.get("body").and_then(|v| v.as_str()) {
534                    *response_body = body.to_string();
535                }
536            }
537
538            match answer {
539                Some((status_code, response_headers, response_body)) => {
540                    // A model-chosen 4xx is `send_error_response` — a real refusal, not a
541                    // netget failure. Tag it separately so an operator can tell a denial
542                    // from an outage.
543                    let decision = if status_code >= 400 {
544                        "model_reject"
545                    } else {
546                        "model_answer"
547                    };
548                    debug!(
549                        "SAML SP {} {} from {} decision={} status={}",
550                        method, path, remote_addr, decision, status_code
551                    );
552                    build_safe_response(status_code, response_headers, response_body)
553                }
554                None => {
555                    // Either no actions at all, or actions that carried no usable HTTP
556                    // response. Both are "the model answered nothing" — fail closed with a
557                    // 500 and a category, never a 2xx.
558                    Log::new(Some(&status_tx)).warn(format!(
559                        "SAML SP {} {} from {} decision=fail_closed_no_answer status=500 \
560                         (no usable action output)",
561                        method, path, remote_addr
562                    ));
563                    build_safe_response(
564                        500,
565                        [(
566                            "content-type".to_string(),
567                            "text/plain; charset=utf-8".to_string(),
568                        )],
569                        crate::utils::WireFailure::Unavailable
570                            .prefixed_text()
571                            .to_string(),
572                    )
573                }
574            }
575        }
576        Err(e) => {
577            // SAML rides on HTTP here, and the failure is ours rather than the peer's, so it
578            // is a 5xx: 503 while the backend is saturated so the peer retries, 500
579            // otherwise. Critically it is not a SAML Response at all - a 2xx carrying an
580            // assertion is the only thing an SP will accept as a sign-in, and no branch on
581            // this path can produce one.
582            //
583            // The body is `WireFailure`'s `&'static str` category, never the error: the peer
584            // is an untrusted browser and the error names the backend, the model and netget's
585            // own retry machinery. The full error goes to the log and the status stream.
586            let failure = crate::utils::WireFailure::classify(&e);
587            let overloaded = failure.is_overloaded();
588            let status = if overloaded { 503 } else { 500 };
589            Log::new(Some(&status_tx)).error(format!(
590                "SAML SP {} {} from {} decision=fail_closed_llm_error status={} overload={}: {}",
591                method, path, remote_addr, status, overloaded, e
592            ));
593            let mut headers = vec![(
594                "content-type".to_string(),
595                "text/plain; charset=utf-8".to_string(),
596            )];
597            if overloaded {
598                // Tell the browser/IDP to back off rather than record a permanent fault.
599                headers.push(("retry-after".to_string(), "5".to_string()));
600            }
601            build_safe_response(status, headers, failure.prefixed_text().to_string())
602        }
603    };
604
605    // Update bytes sent
606    let response_size = response.body().size_hint().exact().unwrap_or(0);
607    app_state
608        .update_connection_stats(
609            server_id,
610            connection_id,
611            None,
612            Some(response_size),
613            None,
614            None,
615        )
616        .await;
617
618    debug!(
619        "SAML SP response: {} ({} bytes)",
620        response.status(),
621        response_size
622    );
623
624    Ok(response)
625}