Skip to main content

netget/client/sip/
mod.rs

1//! SIP client implementation
2pub mod actions;
3
4pub use actions::SipClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::net::UdpSocket;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{error, info, trace, warn};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::sip::actions::{SIP_CLIENT_CONNECTED_EVENT, SIP_CLIENT_RESPONSE_RECEIVED_EVENT};
15use crate::llm::ollama_client::OllamaClient;
16use crate::llm::ClientLlmResult;
17use crate::protocol::Event;
18use crate::state::app_state::AppState;
19use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
20use crate::state::{AccessLogOwner, ClientId, ClientStatus};
21
22/// Connection state for LLM processing
23#[derive(Debug, Clone, PartialEq)]
24enum ConnectionState {
25    Idle,
26    Processing,
27    /// Reserved for future TCP support where partial messages may need buffering
28    #[allow(dead_code)]
29    Accumulating,
30}
31
32/// Per-client data for LLM handling
33struct ClientData {
34    state: ConnectionState,
35    queued_data: Vec<u8>,
36    memory: String,
37    call_id: Option<String>,
38    from_tag: Option<String>,
39    to_tag: Option<String>,
40    cseq: u32,
41    /// This socket's own address, for Via and Contact. Held here because
42    /// `execute_sip_action` is reached from three places (connect, read loop, injected
43    /// command) and all three already take the `ClientData` lock to build a request.
44    local_addr: SocketAddr,
45}
46
47/// SIP client that connects to a remote SIP server
48pub struct SipClient;
49
50impl SipClient {
51    /// Connect to a SIP server with integrated LLM actions
52    pub async fn connect_with_llm_actions(
53        remote_addr: String,
54        llm_client: OllamaClient,
55        app_state: Arc<AppState>,
56        status_tx: mpsc::UnboundedSender<String>,
57        client_id: ClientId,
58    ) -> Result<SocketAddr> {
59        // Parse remote address
60        let remote_sock_addr: SocketAddr = remote_addr
61            .parse()
62            .context(format!("Invalid SIP server address: {}", remote_addr))?;
63
64        // Bind to local UDP socket (ephemeral port)
65        let socket = Arc::new(UdpSocket::bind("0.0.0.0:0").await?);
66
67        // Connect UDP socket to remote address
68        socket.connect(remote_sock_addr).await?;
69
70        // Read the local address *after* connect. Before it, the socket is bound to
71        // 0.0.0.0 and `local_addr()` says so; after it, the kernel has picked the outbound
72        // interface and reports the address a peer would actually see. This is the address
73        // that goes in Via and Contact, so taking it a line too early put a wildcard on the
74        // wire.
75        let local_addr = socket.local_addr()?;
76
77        info!(
78            "SIP client {} connected to {} (local: {})",
79            client_id, remote_sock_addr, local_addr
80        );
81
82        // Update client state
83        app_state
84            .update_client_status(client_id, ClientStatus::Connected)
85            .await;
86        let _ = status_tx.send(format!("[CLIENT] SIP client {} connected", client_id));
87        let _ = status_tx.send("__UPDATE_UI__".to_string());
88
89        // Initialize client data
90        let client_data = Arc::new(Mutex::new(ClientData {
91            state: ConnectionState::Idle,
92            queued_data: Vec::new(),
93            memory: String::new(),
94            call_id: None,
95            from_tag: None,
96            to_tag: None,
97            cseq: 1,
98            local_addr,
99        }));
100
101        // Call LLM with initial connected event
102        let protocol = Arc::new(crate::client::sip::actions::SipClientProtocol::new());
103        let event = Event::new(
104            &SIP_CLIENT_CONNECTED_EVENT,
105            serde_json::json!({
106                "remote_addr": remote_sock_addr.to_string(),
107                "local_addr": local_addr.to_string(),
108            }),
109        );
110
111        // Command channel for injected actions (the dashboard's [ send ]).
112        // Registered BEFORE the connected-event LLM call: a manual `*` routing rule can
113        // park that call for minutes, and the operator must be able to reach the client
114        // while it waits. `recv` is cancellation-safe, so a `select!` arm in the read loop
115        // would also be sound - but the read loop can sit inside a multi-minute LLM call,
116        // and injected commands must not queue behind it. Hence a separate task; the UDP
117        // socket is an `Arc<UdpSocket>` and `send` takes `&self`, so both tasks can write.
118        let command_rx =
119            crate::client::command_support::register_command_channel(&app_state, client_id).await;
120        let cmd_task = tokio::spawn(Self::command_loop(
121            command_rx,
122            socket.clone(),
123            protocol.clone(),
124            client_data.clone(),
125            client_id,
126            app_state.clone(),
127            status_tx.clone(),
128        ));
129        app_state.register_client_task(client_id, cmd_task).await;
130
131        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
132            // Copy the memory out and drop the guard: passing `&guard.memory` straight into the
133            // call would hold the mutex for the whole LLM round-trip, and an injected command
134            // (which needs the same mutex for the Call-ID / tag / CSeq state) would queue behind
135            // it - defeating the point of running the command loop as its own task.
136            let memory = client_data.lock().await.memory.clone();
137            match call_llm_for_client(
138                &llm_client,
139                &app_state,
140                client_id.to_string(),
141                &instruction,
142                &memory,
143                Some(&event),
144                protocol.as_ref(),
145                &status_tx,
146            )
147            .await
148            {
149                Ok(ClientLlmResult {
150                    actions,
151                    memory_updates,
152                }) => {
153                    // Update memory
154                    if let Some(mem) = memory_updates {
155                        client_data.lock().await.memory = mem;
156                    }
157
158                    // Execute initial actions (e.g., REGISTER)
159                    for action in actions {
160                        if let Err(e) = Self::execute_sip_action(
161                            action,
162                            &socket,
163                            &protocol,
164                            &client_data,
165                            client_id,
166                            &status_tx,
167                        )
168                        .await
169                        {
170                            error!("SIP client {} send error: {}", client_id, e);
171                        }
172                    }
173                }
174                Err(e) => {
175                    error!("LLM error for SIP client {}: {}", client_id, e);
176                }
177            }
178        }
179
180        // Spawn read loop
181        let socket_clone = socket.clone();
182        let llm_clone = llm_client.clone();
183        let state_clone = app_state.clone();
184        let status_clone = status_tx.clone();
185        let protocol_clone = protocol.clone();
186        let client_data_clone = client_data.clone();
187
188        // Registered with AppState so stop_client can abort this task —
189        // dropping a JoinHandle only detaches it in Tokio.
190        let task_registrar = app_state.clone();
191        let task_handle = tokio::spawn(async move {
192            let mut buffer = vec![0u8; 65535]; // Max UDP packet size
193
194            loop {
195                match socket_clone.recv(&mut buffer).await {
196                    Ok(n) => {
197                        let data = buffer[..n].to_vec();
198                        trace!("SIP client {} received {} bytes", client_id, n);
199
200                        // Parse SIP response
201                        let response = match Self::parse_sip_response(&data) {
202                            Ok(resp) => resp,
203                            Err(e) => {
204                                warn!("SIP client {} failed to parse response: {}", client_id, e);
205                                continue;
206                            }
207                        };
208
209                        info!(
210                            "SIP client {} received {} response (status: {})",
211                            client_id, response.method, response.status_code
212                        );
213
214                        // Handle provisional responses (1xx) without calling LLM
215                        if response.status_code >= 100 && response.status_code < 200 {
216                            info!(
217                                "SIP client {} received provisional response {} {}, skipping LLM",
218                                client_id, response.status_code, response.reason_phrase
219                            );
220                            let _ = status_clone.send(format!(
221                                "[CLIENT] SIP {} response: {} {}",
222                                client_id, response.status_code, response.reason_phrase
223                            ));
224                            continue;
225                        }
226
227                        // Handle response with LLM
228                        let mut client_data_lock = client_data_clone.lock().await;
229
230                        match client_data_lock.state {
231                            ConnectionState::Idle => {
232                                // Process immediately
233                                client_data_lock.state = ConnectionState::Processing;
234
235                                // Extract To tag if present
236                                if let Some(to_tag) = &response.to_tag {
237                                    client_data_lock.to_tag = Some(to_tag.clone());
238                                }
239
240                                drop(client_data_lock);
241
242                                // Call LLM
243                                if let Some(instruction) =
244                                    state_clone.get_instruction_for_client(client_id).await
245                                {
246                                    let event = Event::new(
247                                        &SIP_CLIENT_RESPONSE_RECEIVED_EVENT,
248                                        serde_json::json!({
249                                            "status_code": response.status_code,
250                                            "reason_phrase": response.reason_phrase,
251                                            "method": response.method,
252                                            "call_id": response.call_id,
253                                            "from": response.from,
254                                            "to": response.to,
255                                            "body": response.body,
256                                        }),
257                                    );
258
259                                    // Copied out so the mutex is not held across the LLM
260                                    // round-trip (see the connect path).
261                                    let memory = client_data_clone.lock().await.memory.clone();
262                                    match call_llm_for_client(
263                                        &llm_clone,
264                                        &state_clone,
265                                        client_id.to_string(),
266                                        &instruction,
267                                        &memory,
268                                        Some(&event),
269                                        protocol_clone.as_ref(),
270                                        &status_clone,
271                                    )
272                                    .await
273                                    {
274                                        Ok(ClientLlmResult {
275                                            actions,
276                                            memory_updates,
277                                        }) => {
278                                            // Update memory
279                                            if let Some(mem) = memory_updates {
280                                                client_data_clone.lock().await.memory = mem;
281                                            }
282
283                                            // Execute actions
284                                            for action in actions {
285                                                if let Err(e) = Self::execute_sip_action(
286                                                    action,
287                                                    &socket_clone,
288                                                    &protocol_clone,
289                                                    &client_data_clone,
290                                                    client_id,
291                                                    &status_clone,
292                                                )
293                                                .await
294                                                {
295                                                    error!(
296                                                        "SIP client {} send error: {}",
297                                                        client_id, e
298                                                    );
299                                                }
300                                            }
301                                        }
302                                        Err(e) => {
303                                            error!("LLM error for SIP client {}: {}", client_id, e);
304                                        }
305                                    }
306                                }
307
308                                // Send automatic ACK for 200 OK response to INVITE (RFC 3261)
309                                if response.method == "INVITE" && response.status_code == 200 {
310                                    info!(
311                                        "SIP client {} sending automatic ACK for INVITE 200 OK",
312                                        client_id
313                                    );
314
315                                    // Both of these were `.unwrap()`, and both are `None` until
316                                    // this client has itself sent a request. A datagram whose
317                                    // CSeq says `INVITE` and whose status line says 200 — which
318                                    // any peer can send unprompted — therefore panicked the read
319                                    // loop, inside a `tokio::spawn` that swallows the panic: the
320                                    // client stayed "Connected" and went permanently deaf.
321                                    // There is no dialog to acknowledge in that case, so the
322                                    // honest answer is to log it and send nothing.
323                                    let ack = {
324                                        let d = client_data_clone.lock().await;
325                                        match (d.call_id.as_ref(), d.from_tag.as_ref()) {
326                                            (Some(call_id), Some(from_tag)) => {
327                                                Some(Self::build_ack_request(
328                                                    &response,
329                                                    call_id,
330                                                    from_tag,
331                                                    // Same CSeq as the INVITE. Saturating
332                                                    // because `cseq` is unsigned and nothing
333                                                    // guarantees a request preceded this.
334                                                    d.cseq.saturating_sub(1),
335                                                    d.local_addr,
336                                                ))
337                                            }
338                                            _ => None,
339                                        }
340                                    };
341                                    match ack {
342                                        None => warn!(
343                                            "SIP client {} got an INVITE 200 OK with no local \
344                                             dialog (no Call-ID or From tag yet); this client \
345                                             sent no INVITE, so no ACK is owed",
346                                            client_id
347                                        ),
348                                        Some(ack_request) => {
349                                            match socket_clone.send(&ack_request).await {
350                                                Ok(sent) => {
351                                                    info!(
352                                                        "SIP client {} sent ACK ({} bytes)",
353                                                        client_id, sent
354                                                    );
355                                                    let _ = status_clone.send(format!(
356                                                        "[CLIENT] SIP client {} sent ACK",
357                                                        client_id
358                                                    ));
359                                                }
360                                                Err(e) => {
361                                                    error!(
362                                                        "SIP client {} ACK send error: {}",
363                                                        client_id, e
364                                                    );
365                                                }
366                                            }
367                                        }
368                                    }
369                                }
370
371                                // Reset state
372                                let mut lock = client_data_clone.lock().await;
373                                lock.state = ConnectionState::Idle;
374
375                                // Process queued data if any
376                                if !lock.queued_data.is_empty() {
377                                    let queued = lock.queued_data.clone();
378                                    lock.queued_data.clear();
379                                    drop(lock);
380
381                                    info!(
382                                        "SIP client {} processing {} bytes of queued data",
383                                        client_id,
384                                        queued.len()
385                                    );
386
387                                    // Parse and process queued response
388                                    if let Ok(queued_response) = Self::parse_sip_response(&queued) {
389                                        // Skip provisional responses in queue
390                                        if queued_response.status_code >= 200 {
391                                            // Set state to Processing for queued response
392                                            client_data_clone.lock().await.state =
393                                                ConnectionState::Processing;
394
395                                            // Extract To tag if present
396                                            if let Some(to_tag) = &queued_response.to_tag {
397                                                client_data_clone.lock().await.to_tag =
398                                                    Some(to_tag.clone());
399                                            }
400
401                                            // Call LLM for queued response
402                                            if let Some(instruction) = state_clone
403                                                .get_instruction_for_client(client_id)
404                                                .await
405                                            {
406                                                let event = Event::new(
407                                                    &SIP_CLIENT_RESPONSE_RECEIVED_EVENT,
408                                                    serde_json::json!({
409                                                        "status_code": queued_response.status_code,
410                                                        "reason_phrase": queued_response.reason_phrase,
411                                                        "method": queued_response.method,
412                                                        "call_id": queued_response.call_id,
413                                                        "from": queued_response.from,
414                                                        "to": queued_response.to,
415                                                        "body": queued_response.body,
416                                                    }),
417                                                );
418
419                                                // Copied out so the mutex is not held across
420                                                // the LLM round-trip (see the connect path).
421                                                let memory =
422                                                    client_data_clone.lock().await.memory.clone();
423                                                match call_llm_for_client(
424                                                    &llm_clone,
425                                                    &state_clone,
426                                                    client_id.to_string(),
427                                                    &instruction,
428                                                    &memory,
429                                                    Some(&event),
430                                                    protocol_clone.as_ref(),
431                                                    &status_clone,
432                                                )
433                                                .await
434                                                {
435                                                    Ok(ClientLlmResult {
436                                                        actions,
437                                                        memory_updates,
438                                                    }) => {
439                                                        if let Some(mem) = memory_updates {
440                                                            client_data_clone.lock().await.memory =
441                                                                mem;
442                                                        }
443
444                                                        for action in actions {
445                                                            if let Err(e) =
446                                                                Self::execute_sip_action(
447                                                                    action,
448                                                                    &socket_clone,
449                                                                    &protocol_clone,
450                                                                    &client_data_clone,
451                                                                    client_id,
452                                                                    &status_clone,
453                                                                )
454                                                                .await
455                                                            {
456                                                                error!(
457                                                                    "SIP client {} send error: {}",
458                                                                    client_id, e
459                                                                );
460                                                            }
461                                                        }
462                                                    }
463                                                    Err(e) => {
464                                                        error!("LLM error for queued SIP response {}: {}", client_id, e);
465                                                    }
466                                                }
467                                            }
468
469                                            // Reset state after processing queued response
470                                            client_data_clone.lock().await.state =
471                                                ConnectionState::Idle;
472                                        }
473                                    }
474                                }
475                            }
476                            ConnectionState::Processing => {
477                                // Queue data
478                                client_data_lock.queued_data.extend_from_slice(&data);
479                            }
480                            ConnectionState::Accumulating => {
481                                // Accumulate data (not used for UDP)
482                            }
483                        }
484                    }
485                    Err(e) => {
486                        error!("SIP client {} recv error: {}", client_id, e);
487                        state_clone
488                            .update_client_status(client_id, ClientStatus::Error(e.to_string()))
489                            .await;
490                        let _ = status_clone
491                            .send(format!("[CLIENT] SIP client {} error: {}", client_id, e));
492                        let _ = status_clone.send("__UPDATE_UI__".to_string());
493                        break;
494                    }
495                }
496            }
497            // Every exit path lands here: drop the command handle so the dashboard stops
498            // offering [ send ] on a dead client. This also closes the command channel,
499            // which ends `command_loop`.
500            state_clone.remove_client_handle(client_id).await;
501            let _ = status_clone.send("__UPDATE_UI__".to_string());
502        });
503        task_registrar
504            .register_client_task(client_id, task_handle)
505            .await;
506
507        Ok(local_addr)
508    }
509
510    /// Drain injected commands until the channel closes (client removed, or the read loop
511    /// exited and dropped the handle) or an injected `disconnect` ends the session.
512    ///
513    /// The generic `command_support::handle_stream_client_command` cannot run this client's
514    /// vocabulary - every SIP verb yields `ClientActionResult::Custom` and the request line,
515    /// Call-ID, tags and CSeq come from per-client state - so the action goes through
516    /// [`Self::execute_sip_action`], the same function the LLM path uses, and the outcome is
517    /// recorded and replied exactly the way the generic arm does it.
518    async fn command_loop(
519        mut command_rx: mpsc::Receiver<ClientCommand>,
520        socket: Arc<UdpSocket>,
521        protocol: Arc<SipClientProtocol>,
522        client_data: Arc<Mutex<ClientData>>,
523        client_id: ClientId,
524        app_state: Arc<AppState>,
525        status_tx: mpsc::UnboundedSender<String>,
526    ) {
527        use crate::llm::actions::protocol_trait::Protocol;
528
529        while let Some(command) = command_rx.recv().await {
530            let action = command.action.clone();
531            let outcome = Self::execute_sip_action(
532                action.clone(),
533                &socket,
534                &protocol,
535                &client_data,
536                client_id,
537                &status_tx,
538            )
539            .await;
540
541            let outcome_json = match &outcome {
542                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
543                Err(e) => serde_json::json!({"error": e.to_string()}),
544            };
545            app_state
546                .record_access_log(
547                    AccessLogOwner::Client(client_id.as_u32()),
548                    protocol.protocol_name(),
549                    None,
550                    "injected_action",
551                    action,
552                    vec![outcome_json],
553                )
554                .await;
555
556            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
557            if let Err(e) = &outcome {
558                error!("SIP client {} injected action failed: {}", client_id, e);
559                let _ = status_tx.send(format!(
560                    "[WARN] Client {} injected action failed: {}",
561                    client_id, e
562                ));
563            }
564            let _ = status_tx.send("__UPDATE_UI__".to_string());
565            crate::client::command_support::reply(command, outcome);
566
567            if disconnect {
568                // SIP over UDP has no connection to close, so the honest end of an injected
569                // `disconnect` is: stop accepting commands, drop the handle, and mark the
570                // client disconnected. The recv loop's socket is released when the client is
571                // removed (`stop_client` aborts both registered tasks).
572                app_state.remove_client_handle(client_id).await;
573                app_state
574                    .update_client_status(client_id, ClientStatus::Disconnected)
575                    .await;
576                let _ = status_tx.send("__UPDATE_UI__".to_string());
577                break;
578            }
579        }
580    }
581
582    /// Execute one SIP action and report what actually reached the wire.
583    ///
584    /// Shared by the connected-event path, the read loop and injected commands, so the SIP
585    /// request encoding (and the Call-ID / tag / CSeq bookkeeping that goes with it) exists
586    /// exactly once. `Err` means the datagram could not be sent; the outcome variants are
587    /// the truthful report of what happened otherwise.
588    async fn execute_sip_action(
589        action: serde_json::Value,
590        socket: &Arc<UdpSocket>,
591        protocol: &Arc<SipClientProtocol>,
592        client_data: &Arc<Mutex<ClientData>>,
593        client_id: ClientId,
594        status_tx: &mpsc::UnboundedSender<String>,
595    ) -> Result<ClientSendOutcome> {
596        use crate::llm::actions::client_trait::{Client, ClientActionResult};
597
598        let result = match protocol.as_ref().execute_action(action.clone()) {
599            Ok(result) => result,
600            Err(e) => {
601                error!("Action execution error for SIP client {}: {}", client_id, e);
602                return Ok(ClientSendOutcome::Rejected {
603                    error: e.to_string(),
604                });
605            }
606        };
607
608        match result {
609            ClientActionResult::Custom { name, data } => {
610                match name.as_str() {
611                    "sip_register" | "sip_invite" | "sip_ack" | "sip_bye" | "sip_options"
612                    | "sip_cancel" => {
613                        // Build and send SIP request
614                        let mut client_data_lock = client_data.lock().await;
615
616                        // Generate Call-ID if not set
617                        if client_data_lock.call_id.is_none() {
618                            client_data_lock.call_id = Some(Self::generate_call_id());
619                        }
620
621                        // Generate From tag if not set
622                        if client_data_lock.from_tag.is_none() {
623                            client_data_lock.from_tag = Some(Self::generate_tag());
624                        }
625
626                        // `expect` rather than `unwrap`: both were just filled in above, so
627                        // this cannot be reached, and saying so is cheaper than making the
628                        // reader prove it.
629                        let request = Self::build_sip_request(
630                            &name,
631                            &data,
632                            client_data_lock
633                                .call_id
634                                .as_ref()
635                                .expect("Call-ID generated above"),
636                            client_data_lock
637                                .from_tag
638                                .as_ref()
639                                .expect("From tag generated above"),
640                            client_data_lock.to_tag.as_ref(),
641                            client_data_lock.cseq,
642                            client_data_lock.local_addr,
643                        );
644
645                        client_data_lock.cseq += 1;
646                        drop(client_data_lock);
647
648                        // Send request
649                        let sent = socket
650                            .send(&request)
651                            .await
652                            .with_context(|| format!("SIP client {} send failed", client_id))?;
653                        info!("SIP client {} sent {} bytes", client_id, sent);
654                        let _ = status_tx.send(format!(
655                            "[CLIENT] SIP client {} sent {} request",
656                            client_id, name
657                        ));
658                        Ok(ClientSendOutcome::Sent { bytes_sent: sent })
659                    }
660                    other => Ok(ClientSendOutcome::Executed {
661                        detail: format!("custom result '{other}' builds no SIP request"),
662                    }),
663                }
664            }
665            ClientActionResult::Disconnect => {
666                info!("SIP client {} disconnecting", client_id);
667                let _ = status_tx.send(format!("[CLIENT] SIP client {} disconnecting", client_id));
668                Ok(ClientSendOutcome::Disconnected)
669            }
670            ClientActionResult::WaitForMore => {
671                trace!("SIP client {} waiting for more data", client_id);
672                Ok(ClientSendOutcome::Executed {
673                    detail: "wait_for_more".to_string(),
674                })
675            }
676            other => Ok(ClientSendOutcome::Executed {
677                detail: format!("{other:?} sends no SIP request"),
678            }),
679        }
680    }
681
682    /// Parse SIP response from bytes
683    fn parse_sip_response(data: &[u8]) -> Result<SipResponse> {
684        let text = String::from_utf8(data.to_vec())?;
685        let lines: Vec<&str> = text.lines().collect();
686
687        if lines.is_empty() {
688            anyhow::bail!("Empty SIP response");
689        }
690
691        // Parse status line (e.g., "SIP/2.0 200 OK")
692        let status_line: Vec<&str> = lines[0].split_whitespace().collect();
693        if status_line.len() < 3 {
694            anyhow::bail!("Invalid SIP status line");
695        }
696
697        let status_code: u16 = status_line[1].parse()?;
698        let reason_phrase = status_line[2..].join(" ");
699
700        // Parse headers
701        let mut call_id = String::new();
702        let mut from = String::new();
703        let mut to = String::new();
704        let mut cseq = String::new();
705        let mut to_tag = None;
706        let mut content_length = 0;
707
708        let mut i = 1;
709        while i < lines.len() {
710            let line = lines[i];
711            if line.is_empty() {
712                // End of headers
713                i += 1;
714                break;
715            }
716
717            if let Some(colon_pos) = line.find(':') {
718                let (header_name, header_value) = line.split_at(colon_pos);
719                let header_value = header_value[1..].trim();
720
721                match header_name.to_lowercase().as_str() {
722                    "call-id" => call_id = header_value.to_string(),
723                    "from" | "f" => from = header_value.to_string(),
724                    "to" | "t" => {
725                        to = header_value.to_string();
726                        // Extract tag if present
727                        if let Some(tag_start) = header_value.find(";tag=") {
728                            to_tag = Some(header_value[tag_start + 5..].to_string());
729                        }
730                    }
731                    "cseq" => cseq = header_value.to_string(),
732                    "content-length" | "l" => content_length = header_value.parse().unwrap_or(0),
733                    _ => {}
734                }
735            }
736
737            i += 1;
738        }
739
740        // Parse body (SDP or other content)
741        let mut body = String::new();
742        if content_length > 0 && i < lines.len() {
743            body = lines[i..].join("\r\n");
744        }
745
746        // Extract method from CSeq
747        let method = cseq.split_whitespace().nth(1).unwrap_or("").to_string();
748
749        Ok(SipResponse {
750            status_code,
751            reason_phrase,
752            method,
753            call_id,
754            from,
755            to,
756            to_tag,
757            cseq,
758            body: if body.is_empty() { None } else { Some(body) },
759        })
760    }
761
762    /// Build SIP request from action data
763    fn build_sip_request(
764        method: &str,
765        data: &serde_json::Value,
766        call_id: &str,
767        from_tag: &str,
768        to_tag: Option<&String>,
769        cseq: u32,
770        local_addr: SocketAddr,
771    ) -> Vec<u8> {
772        let from = data["from"].as_str().unwrap_or("sip:user@localhost");
773        let to = data["to"].as_str().unwrap_or("sip:server@localhost");
774        let request_uri = data["request_uri"]
775            .as_str()
776            .unwrap_or("sip:server@localhost");
777
778        // Map action name to SIP method
779        let sip_method = match method {
780            "sip_register" => "REGISTER",
781            "sip_invite" => "INVITE",
782            "sip_ack" => "ACK",
783            "sip_bye" => "BYE",
784            "sip_options" => "OPTIONS",
785            "sip_cancel" => "CANCEL",
786            _ => "OPTIONS",
787        };
788
789        // Build request line
790        let mut request = format!("{} {} SIP/2.0\r\n", sip_method, request_uri);
791
792        // Add Via header.
793        //
794        // This is our own address, not the literal `127.0.0.1:5060` it used to be. Via is where
795        // an RFC 3261 server sends the response, so a hardcoded loopback address meant every
796        // reply from a real server went to port 5060 on the server's own loopback and this
797        // client never saw it. It worked only against netget's own SIP server, which answers
798        // the datagram's source address instead. `;rport` (RFC 3581) additionally asks the
799        // server to reply to the source address and port it actually observed, which is what
800        // makes this work from behind NAT.
801        request.push_str(&format!(
802            "Via: SIP/2.0/UDP {};rport;branch=z9hG4bK-netget-{}\r\n",
803            local_addr,
804            Self::generate_branch()
805        ));
806
807        // Add From header (with tag)
808        request.push_str(&format!("From: <{}>;tag={}\r\n", from, from_tag));
809
810        // Add To header (with tag if available)
811        if let Some(tag) = to_tag {
812            request.push_str(&format!("To: <{}>;tag={}\r\n", to, tag));
813        } else {
814            request.push_str(&format!("To: <{}>\r\n", to));
815        }
816
817        // Add Call-ID header
818        request.push_str(&format!("Call-ID: {}\r\n", call_id));
819
820        // Add CSeq header
821        request.push_str(&format!("CSeq: {} {}\r\n", cseq, sip_method));
822
823        // Add Contact header (for REGISTER/INVITE)
824        if sip_method == "REGISTER" || sip_method == "INVITE" {
825            // Default Contact is this socket's own address for the same reason as Via: a
826            // server that calls back a hardcoded 127.0.0.1 reaches itself, not us.
827            let default_contact = format!("sip:user@{}", local_addr);
828            let contact = data["contact"].as_str().unwrap_or(&default_contact);
829            request.push_str(&format!("Contact: <{}>\r\n", contact));
830        }
831
832        // Add Expires header (for REGISTER)
833        if sip_method == "REGISTER" {
834            let expires = data["expires"].as_u64().unwrap_or(3600);
835            request.push_str(&format!("Expires: {}\r\n", expires));
836        }
837
838        // Add body (SDP for INVITE)
839        let body = if sip_method == "INVITE" {
840            data["sdp"].as_str()
841        } else {
842            None
843        };
844
845        // Add Content-Length and body
846        if let Some(body_text) = body {
847            request.push_str("Content-Type: application/sdp\r\n");
848            request.push_str(&format!("Content-Length: {}\r\n", body_text.len()));
849            request.push_str("\r\n");
850            request.push_str(body_text);
851        } else {
852            request.push_str("Content-Length: 0\r\n");
853            request.push_str("\r\n");
854        }
855
856        request.into_bytes()
857    }
858
859    /// Build ACK request for INVITE 3-way handshake (RFC 3261)
860    fn build_ack_request(
861        response: &SipResponse,
862        call_id: &str,
863        from_tag: &str,
864        cseq: u32,
865        local_addr: SocketAddr,
866    ) -> Vec<u8> {
867        // Extract URIs from response headers
868        let from_uri = Self::extract_uri(&response.from).unwrap_or("sip:user@localhost");
869        let to_uri = Self::extract_uri(&response.to).unwrap_or("sip:server@localhost");
870
871        // ACK uses same Request-URI as INVITE, which is the To URI
872        let request_uri = to_uri;
873
874        // Build request line
875        let mut request = format!("ACK {} SIP/2.0\r\n", request_uri);
876
877        // Add Via header.
878        //
879        // This is our own address, not the literal `127.0.0.1:5060` it used to be. Via is where
880        // an RFC 3261 server sends the response, so a hardcoded loopback address meant every
881        // reply from a real server went to port 5060 on the server's own loopback and this
882        // client never saw it. It worked only against netget's own SIP server, which answers
883        // the datagram's source address instead. `;rport` (RFC 3581) additionally asks the
884        // server to reply to the source address and port it actually observed, which is what
885        // makes this work from behind NAT.
886        request.push_str(&format!(
887            "Via: SIP/2.0/UDP {};rport;branch=z9hG4bK-netget-{}\r\n",
888            local_addr,
889            Self::generate_branch()
890        ));
891
892        // Add From header (with tag)
893        request.push_str(&format!("From: <{}>;tag={}\r\n", from_uri, from_tag));
894
895        // Add To header (with tag from response - ACK must include To tag)
896        if let Some(to_tag) = &response.to_tag {
897            request.push_str(&format!("To: <{}>;tag={}\r\n", to_uri, to_tag));
898        } else {
899            request.push_str(&format!("To: <{}>\r\n", to_uri));
900        }
901
902        // Add Call-ID header
903        request.push_str(&format!("Call-ID: {}\r\n", call_id));
904
905        // Add CSeq header (same number as INVITE, but ACK method)
906        request.push_str(&format!("CSeq: {} ACK\r\n", cseq));
907
908        // ACK has no body
909        request.push_str("Content-Length: 0\r\n");
910        request.push_str("\r\n");
911
912        request.into_bytes()
913    }
914
915    /// Extract SIP URI from header value (removes display name and parameters)
916    fn extract_uri(header_value: &str) -> Option<&str> {
917        // Handle format: "Display Name" <sip:user@host>;tag=xyz
918        // or: sip:user@host;tag=xyz
919        // or: <sip:user@host>
920
921        let trimmed = header_value.trim();
922
923        // Check for angle brackets.
924        //
925        // The closing bracket is searched for *after* the opening one. Searching the whole
926        // string for '>' independently panicked whenever a '>' appeared first — which a peer
927        // can arrange with a display name such as `"a>b" <sip:x@y>` — because `&s[start+1..end]`
928        // with `start > end` is a slice whose start is past its end. Remotely triggerable, in
929        // the read loop, inside a `tokio::spawn` that swallows the panic.
930        if let Some(start) = trimmed.find('<') {
931            if let Some(offset) = trimmed[start + 1..].find('>') {
932                return Some(&trimmed[start + 1..start + 1 + offset]);
933            }
934        }
935
936        // No angle brackets, extract until semicolon or end
937        if let Some(semicolon) = trimmed.find(';') {
938            return Some(&trimmed[..semicolon]);
939        }
940
941        Some(trimmed)
942    }
943
944    /// Generate a random Call-ID
945    fn generate_call_id() -> String {
946        use rand::Rng;
947        let random: u64 = rand::thread_rng().gen();
948        format!("{}@netget-client", random)
949    }
950
951    /// Generate a random tag
952    fn generate_tag() -> String {
953        use rand::Rng;
954        let tag: u32 = rand::thread_rng().gen();
955        format!("{:x}", tag)
956    }
957
958    /// Generate a random branch parameter
959    fn generate_branch() -> String {
960        use rand::Rng;
961        let branch: u32 = rand::thread_rng().gen();
962        format!("{:x}", branch)
963    }
964}
965
966/// Parsed SIP response
967#[derive(Debug, Clone)]
968struct SipResponse {
969    status_code: u16,
970    reason_phrase: String,
971    method: String, // Extracted from CSeq
972    call_id: String,
973    from: String,
974    to: String,
975    to_tag: Option<String>,
976    /// Full CSeq header value (kept for debugging and future use)
977    #[allow(dead_code)]
978    cseq: String,
979    body: Option<String>,
980}