1pub mod actions;
5pub mod wire;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, EventType, SpawnContext};
11use crate::server::connection::ConnectionId;
12use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
13use anyhow::{Context, Result};
14use serde_json::{json, Value};
15use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
16use std::sync::Arc;
17use std::{net::SocketAddr, time::Duration};
18use tokio::{
19 io::{AsyncWriteExt, WriteHalf},
20 net::{TcpListener, TcpStream},
21};
22use wire::Pdu;
23
24pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
25pub const MAX_IDLE_TIMEOUT_SECS: u64 = 86400;
26pub const DEFAULT_SYSTEM_ID: &str = "NETGET";
27
28#[derive(Clone)]
29struct Config {
30 system_id: String,
31 credentials: Option<(String, String)>,
32 idle: Duration,
33 next_message_id: Arc<AtomicU64>,
34}
35
36fn config(ctx: &SpawnContext) -> Result<Config> {
37 let params = ctx.startup_params.as_ref();
38 let get = |name: &str| -> Result<Option<String>> {
39 Ok(params
40 .map(|p| p.get_optional_string(name))
41 .transpose()?
42 .flatten())
43 };
44 let system_id = get("system_id")?.unwrap_or_else(|| DEFAULT_SYSTEM_ID.into());
45 anyhow::ensure!(
46 !system_id.is_empty() && system_id.len() <= 15,
47 "system_id must be 1 to 15 characters"
48 );
49 let credentials = match (get("esme_system_id")?, get("password")?) {
50 (Some(id), Some(pw)) => {
51 anyhow::ensure!(
52 id.len() <= 15 && pw.len() <= 8,
53 "esme_system_id is at most 15 characters, password at most 8"
54 );
55 Some((id, pw))
56 }
57 (None, None) => None,
58 _ => anyhow::bail!("esme_system_id and password are configured together"),
59 };
60 let secs = params
61 .map(|p| p.get_optional_u64("idle_timeout_secs"))
62 .transpose()?
63 .flatten()
64 .unwrap_or(IDLE_TIMEOUT.as_secs());
65 anyhow::ensure!(
66 (1..=MAX_IDLE_TIMEOUT_SECS).contains(&secs),
67 "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
68 );
69 Ok(Config {
70 system_id,
71 credentials,
72 idle: Duration::from_secs(secs),
73 next_message_id: Arc::new(AtomicU64::new(1)),
74 })
75}
76
77pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
78 let cfg = config(&ctx)?;
79 let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
80 let addr = listener.local_addr()?;
81 Log::new(Some(&ctx.status_tx)).info(format!("SMPP SMSC listening on {addr}"));
82 let state = ctx.state.clone();
83 let server_id = ctx.server_id;
84 let accept = tokio::spawn(async move {
85 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
86 crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
87 );
88 loop {
89 let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
90 &listener,
91 &limiter,
92 b"",
93 "SMPP",
94 Some(&ctx.status_tx),
95 )
96 .await
97 {
98 Ok(v) => v,
99 Err(_) => break,
100 };
101 let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
102 let now = crate::utils::clock::Instant::now();
103 ctx.state
104 .add_connection_to_server(
105 server_id,
106 ConnectionState {
107 id,
108 remote_addr: peer,
109 local_addr: addr,
110 bytes_sent: 0,
111 bytes_received: 0,
112 packets_sent: 0,
113 packets_received: 0,
114 last_activity: now,
115 status: ConnectionStatus::Active,
116 status_changed_at: now,
117 protocol_info: ProtocolConnectionInfo::empty(),
118 },
119 )
120 .await;
121 let child = ctx.clone();
122 let cfg = cfg.clone();
123 ctx.state
124 .spawn_server_task(server_id, async move {
125 let _permit = permit;
126 if let Err(e) = session(&child, id, socket, peer, &cfg).await {
127 Log::new(Some(&child.status_tx))
128 .warn(format!("SMPP connection {id} ended: {e}"));
129 }
130 child
131 .state
132 .update_connection_status(server_id, id, ConnectionStatus::Closed)
133 .await;
134 let _ = child.status_tx.send("__UPDATE_UI__".into());
135 })
136 .await;
137 }
138 });
139 state.register_server_task(server_id, accept).await;
140 Ok(addr)
141}
142
143fn outcome(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
144 let summary = format!("SMPP connection {id} operation={operation} decision={decision}");
145 let log = Log::new(Some(&ctx.status_tx));
146 if decision.starts_with("fail_closed_") || decision == "model_silent" {
147 log.error(summary);
148 } else {
149 log.info(summary);
150 }
151}
152
153async fn decide(
155 ctx: &SpawnContext,
156 id: ConnectionId,
157 event_type: &'static EventType,
158 data: Value,
159 failure_status: u32,
160) -> std::result::Result<(String, Value), u32> {
161 let op = event_type.id.clone();
162 let event = Event::new(event_type, data);
163 let result = match call_llm(
164 &ctx.llm_client,
165 &ctx.state,
166 ctx.server_id,
167 Some(id),
168 &event,
169 &actions::SmppProtocol,
170 )
171 .await
172 {
173 Ok(result) => result,
174 Err(error) => {
175 outcome(ctx, id, &op, "fail_closed_llm_error");
176 return Err(
177 if crate::llm::is_overload_error(&error) && failure_status == wire::ESME_RSYSERR {
178 wire::ESME_RTHROTTLED
179 } else {
180 failure_status
181 },
182 );
183 }
184 };
185 if !result.failures.is_empty() {
186 outcome(ctx, id, &op, "fail_closed_invalid_reply");
187 return Err(failure_status);
188 }
189 let mut found = Vec::new();
190 let mut pending = result.protocol_results;
191 while let Some(result) = pending.pop() {
192 match result {
193 ActionResult::Custom { name, data } if name.starts_with("smpp_") => {
194 found.push((name, data))
195 }
196 ActionResult::Multiple(items) => pending.extend(items),
197 _ => {}
198 }
199 }
200 match found.len() {
201 1 => Ok(found.remove(0)),
202 0 => {
203 outcome(ctx, id, &op, "model_silent");
204 Err(failure_status)
205 }
206 _ => {
207 outcome(ctx, id, &op, "fail_closed_invalid_reply");
208 Err(failure_status)
209 }
210 }
211}
212
213struct Session<'a> {
214 ctx: &'a SpawnContext,
215 id: ConnectionId,
216 writer: WriteHalf<TcpStream>,
217 next_sequence: AtomicU32,
218}
219
220impl Session<'_> {
221 async fn send(&mut self, pdu: Pdu) -> Result<()> {
222 let bytes = pdu.encode();
223 tokio::time::timeout(wire::IO_TIMEOUT, self.writer.write_all(&bytes))
224 .await
225 .context("SMPP write deadline")??;
226 self.ctx
227 .state
228 .update_connection_stats(
229 self.ctx.server_id,
230 self.id,
231 None,
232 Some(bytes.len() as u64),
233 None,
234 Some(1),
235 )
236 .await;
237 Ok(())
238 }
239
240 async fn respond(&mut self, req: &Pdu, status: u32, body: Vec<u8>) -> Result<()> {
241 self.send(Pdu::new(
242 req.command_id | wire::RESP,
243 status,
244 req.sequence,
245 body,
246 ))
247 .await
248 }
249
250 fn sequence(&self) -> u32 {
251 let n = self.next_sequence.fetch_add(1, Ordering::Relaxed);
253 (n % 0x7FFF_FFFF) + 1
254 }
255}
256
257fn mode_name(command_id: u32) -> &'static str {
258 match command_id {
259 wire::BIND_RECEIVER => "receiver",
260 wire::BIND_TRANSMITTER => "transmitter",
261 _ => "transceiver",
262 }
263}
264
265fn same_secret(a: &[u8], b: &[u8]) -> bool {
266 a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
267}
268
269async fn session(
270 ctx: &SpawnContext,
271 id: ConnectionId,
272 socket: TcpStream,
273 peer: SocketAddr,
274 cfg: &Config,
275) -> Result<()> {
276 let (mut reader, writer) = tokio::io::split(socket);
277 let mut s = Session {
278 ctx,
279 id,
280 writer,
281 next_sequence: AtomicU32::new(0),
282 };
283 let mut bound: Option<(&'static str, String)> = None;
284 loop {
285 let Some(pdu) = wire::read_pdu(&mut reader, cfg.idle).await? else {
286 return Ok(());
287 };
288 ctx.state
289 .update_connection_stats(
290 ctx.server_id,
291 id,
292 Some(pdu.body.len() as u64 + 16),
293 None,
294 Some(1),
295 None,
296 )
297 .await;
298 match pdu.command_id {
299 wire::BIND_RECEIVER | wire::BIND_TRANSMITTER | wire::BIND_TRANSCEIVER => {
300 if bound.is_some() {
301 s.respond(&pdu, wire::ESME_RALYBND, Vec::new()).await?;
302 continue;
303 }
304 let Ok(bind) = wire::parse_bind(&pdu.body) else {
305 s.send(Pdu::new(
306 wire::GENERIC_NACK,
307 wire::ESME_RINVCMDLEN,
308 pdu.sequence,
309 Vec::new(),
310 ))
311 .await?;
312 return Ok(());
313 };
314 let mode = mode_name(pdu.command_id);
315 let status = match &cfg.credentials {
316 Some((user, pass)) => {
317 if bind.system_id != *user {
318 wire::ESME_RINVSYSID
319 } else if !same_secret(bind.password.as_bytes(), pass.as_bytes()) {
320 wire::ESME_RINVPASWD
321 } else {
322 wire::ESME_ROK
323 }
324 }
325 None => match decide(
326 ctx,
327 id,
328 &actions::BIND_EVENT,
329 json!({
330 "mode": mode,
331 "system_id": bind.system_id,
332 "password": bind.password,
333 "system_type": bind.system_type,
334 "interface_version": bind.interface_version,
335 "remote_addr": peer.to_string(),
336 }),
337 wire::ESME_RBINDFAIL,
338 )
339 .await
340 {
341 Ok((name, _)) if name == "smpp_bind_accept" => {
342 outcome(ctx, id, "smpp_bind", "model_answer");
343 wire::ESME_ROK
344 }
345 Ok((name, data)) if name == "smpp_bind_reject" => {
346 outcome(ctx, id, "smpp_bind", "model_reject");
347 data["status"]
348 .as_str()
349 .and_then(wire::status_code)
350 .unwrap_or(wire::ESME_RBINDFAIL)
351 }
352 Ok(_) => {
353 outcome(ctx, id, "smpp_bind", "fail_closed_invalid_reply");
354 wire::ESME_RBINDFAIL
355 }
356 Err(status) => status,
357 },
358 };
359 let mut body = Vec::new();
360 if status == wire::ESME_ROK {
361 wire::put_cstring(&mut body, &cfg.system_id, 16);
362 }
363 s.respond(&pdu, status, body).await?;
364 if status != wire::ESME_ROK {
365 return Ok(());
366 }
367 Log::new(Some(&ctx.status_tx)).info(format!(
368 "SMPP connection {id}: {} bound as {mode}",
369 bind.system_id
370 ));
371 bound = Some((mode, bind.system_id));
372 }
373 wire::SUBMIT_SM => {
374 let Some((mode, system_id)) = bound.clone() else {
375 s.respond(&pdu, wire::ESME_RINVBNDSTS, vec![0]).await?;
376 continue;
377 };
378 if mode == "receiver" {
379 s.respond(&pdu, wire::ESME_RINVBNDSTS, vec![0]).await?;
380 continue;
381 }
382 let message = match wire::parse_message(&pdu.body) {
383 Ok(m) => m,
384 Err(_) => {
385 s.respond(&pdu, wire::ESME_RINVMSGLEN, vec![0]).await?;
386 continue;
387 }
388 };
389 submit(&mut s, cfg, &pdu, &message, mode, &system_id, peer).await?;
390 }
391 wire::ENQUIRE_LINK => s.respond(&pdu, wire::ESME_ROK, Vec::new()).await?,
392 wire::UNBIND => {
393 s.respond(&pdu, wire::ESME_ROK, Vec::new()).await?;
394 return Ok(());
395 }
396 id if id & wire::RESP != 0 => {} _ => {
398 s.send(Pdu::new(
399 wire::GENERIC_NACK,
400 wire::ESME_RINVCMDID,
401 pdu.sequence,
402 Vec::new(),
403 ))
404 .await?;
405 }
406 }
407 }
408}
409
410async fn submit(
411 s: &mut Session<'_>,
412 cfg: &Config,
413 pdu: &Pdu,
414 message: &wire::Message,
415 mode: &str,
416 system_id: &str,
417 peer: SocketAddr,
418) -> Result<()> {
419 let (ctx, id) = (s.ctx, s.id);
420 let text = wire::decode_text(message.data_coding, &message.payload);
421 let mut data = json!({
422 "system_id": system_id,
423 "source_addr": message.source_addr,
424 "destination_addr": message.destination_addr,
425 "data_coding": message.data_coding,
426 "registered_delivery": message.registered_delivery & 0x03 != 0,
427 "esm_class": message.esm_class,
428 "remote_addr": peer.to_string(),
429 });
430 match &text {
431 Some(t) => data["text"] = json!(t),
432 None => {
433 data["data"] = json!(message
434 .payload
435 .iter()
436 .map(|b| format!("{b:02x}"))
437 .collect::<String>())
438 }
439 }
440 let answer = decide(ctx, id, &actions::SUBMIT_EVENT, data, wire::ESME_RSYSERR).await;
441 let accepted = match answer {
442 Ok((name, data)) if name == "smpp_accept" => {
443 outcome(ctx, id, "smpp_submit", "model_answer");
444 data
445 }
446 Ok((name, data)) if name == "smpp_reject" => {
447 outcome(ctx, id, "smpp_submit", "model_reject");
448 let status = data["status"]
449 .as_str()
450 .and_then(wire::status_code)
451 .unwrap_or(wire::ESME_RSUBMITFAIL);
452 return s.respond(pdu, status, vec![0]).await;
453 }
454 Ok(_) => {
455 outcome(ctx, id, "smpp_submit", "fail_closed_invalid_reply");
456 return s.respond(pdu, wire::ESME_RSYSERR, vec![0]).await;
457 }
458 Err(status) => return s.respond(pdu, status, vec![0]).await,
459 };
460 let message_id = accepted["message_id"]
461 .as_str()
462 .map(str::to_string)
463 .unwrap_or_else(|| {
464 format!(
465 "NG{:08X}",
466 cfg.next_message_id.fetch_add(1, Ordering::Relaxed)
467 )
468 });
469 let mut body = Vec::new();
470 wire::put_cstring(&mut body, &message_id, 65);
471 s.respond(pdu, wire::ESME_ROK, body).await?;
472 if mode == "transmitter" {
473 return Ok(());
474 }
475 let now = chrono::Utc::now().format("%y%m%d%H%M").to_string();
476 if let Some(stat) = accepted["receipt"].as_str() {
477 if message.registered_delivery & 0x03 != 0 {
478 let original = text.clone().unwrap_or_default();
479 let receipt = wire::receipt_text(&message_id, stat, &now, &now, &original);
480 let mut receipted = message_id.as_bytes().to_vec();
481 receipted.push(0);
482 let deliver = wire::Message {
483 source_ton: message.dest_ton,
484 source_npi: message.dest_npi,
485 source_addr: message.destination_addr.clone(),
486 dest_ton: message.source_ton,
487 dest_npi: message.source_npi,
488 destination_addr: message.source_addr.clone(),
489 esm_class: 0x04,
490 data_coding: 0,
491 payload: receipt.into_bytes(),
492 tlvs: vec![
493 (wire::TLV_RECEIPTED_MESSAGE_ID, receipted),
494 (wire::TLV_MESSAGE_STATE, vec![wire::message_state(stat)]),
495 ],
496 ..Default::default()
497 };
498 let seq = s.sequence();
499 s.send(Pdu::new(
500 wire::DELIVER_SM,
501 0,
502 seq,
503 wire::encode_message(&deliver),
504 ))
505 .await?;
506 }
507 }
508 if let Some(reply) = accepted["reply_text"].as_str() {
509 let (coding, payload) = wire::encode_text(reply);
510 let deliver = wire::Message {
511 source_ton: message.dest_ton,
512 source_npi: message.dest_npi,
513 source_addr: message.destination_addr.clone(),
514 dest_ton: message.source_ton,
515 dest_npi: message.source_npi,
516 destination_addr: message.source_addr.clone(),
517 data_coding: coding,
518 payload,
519 ..Default::default()
520 };
521 let seq = s.sequence();
522 s.send(Pdu::new(
523 wire::DELIVER_SM,
524 0,
525 seq,
526 wire::encode_message(&deliver),
527 ))
528 .await?;
529 }
530 Ok(())
531}