1pub mod actions;
4pub mod codec;
5pub mod dict;
6pub mod session;
7
8use crate::llm::action_helper::call_llm;
9use crate::llm::actions::protocol_trait::ActionResult;
10use crate::logging::emit::Log;
11use crate::protocol::{Event, SpawnContext};
12use crate::server::accept_bounded::{accept_bounded, ConnectionLimiter, DEFAULT_MAX_CONNECTIONS};
13use crate::server::connection::ConnectionId;
14use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
15use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
16use crate::utils::clock::Instant;
17use anyhow::Result;
18use codec::{Frame, Message};
19use serde_json::{json, Value};
20use session::Session;
21use std::net::SocketAddr;
22use std::sync::Arc;
23use std::time::Duration;
24use tokio::io::{AsyncReadExt, AsyncWriteExt};
25use tokio::net::TcpStream;
26use tokio::sync::mpsc;
27
28pub const DEFAULT_COMP_ID: &str = "NETGET";
29pub const DEFAULT_BEGIN_STRING: &str = "FIX.4.4";
30pub const LOGON_TIMEOUT: Duration = Duration::from_secs(10);
31pub const BEGIN_STRINGS: &[&str] = &["FIX.4.0", "FIX.4.1", "FIX.4.2", "FIX.4.3", "FIX.4.4"];
32pub const LOGOUT_WAIT: Duration = Duration::from_secs(5);
34const NO_HEARTBEAT: Duration = Duration::from_secs(86_400);
36
37struct Shared {
38 ctx: SpawnContext,
39 comp_id: String,
40 begin: String,
41 logon_timeout: Duration,
42}
43
44pub fn comp_id_ok(s: &str) -> bool {
45 !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_graphic())
46}
47
48pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
49 let p = ctx.startup_params.as_ref();
50 let s = |k: &str, d: &str| -> Result<String> {
51 Ok(p.map(|p| p.get_optional_string(k))
52 .transpose()?
53 .flatten()
54 .unwrap_or_else(|| d.to_owned()))
55 };
56 let comp_id = s("sender_comp_id", DEFAULT_COMP_ID)?;
57 anyhow::ensure!(
58 comp_id_ok(&comp_id),
59 "sender_comp_id is 1 to 64 printable characters without spaces"
60 );
61 let begin = s("begin_string", DEFAULT_BEGIN_STRING)?;
62 anyhow::ensure!(
63 BEGIN_STRINGS.contains(&begin.as_str()),
64 "begin_string is one of {BEGIN_STRINGS:?}"
65 );
66 let logon_timeout = p
67 .map(|p| p.get_optional_u64("logon_timeout_secs"))
68 .transpose()?
69 .flatten()
70 .unwrap_or(LOGON_TIMEOUT.as_secs());
71 anyhow::ensure!(
72 (1..=120).contains(&logon_timeout),
73 "logon_timeout_secs must be 1..=120"
74 );
75 let listener = tokio::net::TcpListener::bind(ctx.legacy_listen_addr()).await?;
76 let local = listener.local_addr()?;
77 Log::new(Some(&ctx.status_tx)).info(format!("FIX acceptor {comp_id} ({begin}) on {local}"));
78 let shared = Arc::new(Shared {
79 ctx: ctx.clone(),
80 comp_id,
81 begin,
82 logon_timeout: Duration::from_secs(logon_timeout),
83 });
84 let server_id = ctx.server_id;
85 let accept = tokio::spawn(async move {
86 let limiter = ConnectionLimiter::new(DEFAULT_MAX_CONNECTIONS);
87 loop {
88 let (stream, peer, permit) =
89 match accept_bounded(&listener, &limiter, b"", "FIX", Some(&shared.ctx.status_tx))
90 .await
91 {
92 Ok(v) => v,
93 Err(_) => break,
94 };
95 let id = ConnectionId::new(shared.ctx.state.get_next_unified_id().await);
96 let now = Instant::now();
97 shared
98 .ctx
99 .state
100 .add_connection_to_server(
101 server_id,
102 ConnectionState {
103 id,
104 remote_addr: peer,
105 local_addr: local,
106 bytes_sent: 0,
107 bytes_received: 0,
108 packets_sent: 0,
109 packets_received: 0,
110 last_activity: now,
111 status: ConnectionStatus::Active,
112 status_changed_at: now,
113 protocol_info: ProtocolConnectionInfo::empty(),
114 },
115 )
116 .await;
117 let child = shared.clone();
118 shared
119 .ctx
120 .state
121 .spawn_server_task(server_id, async move {
122 let _permit = permit;
123 if let Err(e) = connection(&child, id, stream).await {
124 Log::new(Some(&child.ctx.status_tx))
125 .debug(format!("FIX connection {id}: {e:#}"));
126 }
127 child
128 .ctx
129 .state
130 .remove_peer_handle(server_id, id.as_u32())
131 .await;
132 child
133 .ctx
134 .state
135 .update_connection_status(server_id, id, ConnectionStatus::Closed)
136 .await;
137 let _ = child.ctx.status_tx.send("__UPDATE_UI__".into());
138 })
139 .await;
140 }
141 });
142 ctx.state.register_server_task(server_id, accept).await;
143 Ok(local)
144}
145
146fn outcome(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
147 let summary = format!("FIX connection {id} operation={operation} decision={decision}");
148 let log = Log::new(Some(&ctx.status_tx));
149 if decision.starts_with("fail_closed_") || decision == "model_silent" {
150 log.error(summary);
151 } else {
152 log.info(summary);
153 }
154}
155
156async fn ask(
158 shared: &Shared,
159 id: ConnectionId,
160 event: Event,
161 operation: &str,
162) -> Result<Vec<Value>, anyhow::Error> {
163 let ctx = &shared.ctx;
164 let result = match call_llm(
165 &ctx.llm_client,
166 &ctx.state,
167 ctx.server_id,
168 Some(id),
169 &event,
170 &actions::FixProtocol,
171 )
172 .await
173 {
174 Ok(r) => r,
175 Err(e) => {
176 outcome(ctx, id, operation, "fail_closed_llm_error");
177 return Err(e);
178 }
179 };
180 if !result.failures.is_empty() {
181 outcome(ctx, id, operation, "fail_closed_invalid_reply");
182 anyhow::bail!("the handler's answer was invalid");
183 }
184 let mut answers = Vec::new();
185 let mut pending: Vec<ActionResult> = result.protocol_results.into_iter().rev().collect();
186 while let Some(r) = pending.pop() {
187 match r {
188 ActionResult::Custom { data, .. } => answers.push(data),
189 ActionResult::Multiple(items) => pending.extend(items.into_iter().rev()),
190 _ => {}
191 }
192 }
193 if answers.is_empty() {
194 outcome(ctx, id, operation, "model_silent");
195 anyhow::bail!("the handler gave no answer");
196 }
197 Ok(answers)
198}
199
200struct Conn<'a> {
201 shared: &'a Shared,
202 id: ConnectionId,
203 r: tokio::io::ReadHalf<TcpStream>,
204 w: tokio::io::WriteHalf<TcpStream>,
205 buf: Vec<u8>,
206}
207
208impl Conn<'_> {
209 async fn write(&mut self, chunks: &[Vec<u8>]) -> Result<()> {
210 for c in chunks.iter().filter(|c| !c.is_empty()) {
211 self.w.write_all(c).await?;
212 self.shared
213 .ctx
214 .state
215 .update_connection_stats(
216 self.shared.ctx.server_id,
217 self.id,
218 None,
219 Some(c.len() as u64),
220 None,
221 Some(1),
222 )
223 .await;
224 }
225 Ok(())
226 }
227
228 async fn next(&mut self) -> Result<Option<(String, Message)>> {
230 loop {
231 match codec::frame(&self.buf) {
232 Frame::Message {
233 begin,
234 message,
235 consumed,
236 } => {
237 self.buf.drain(..consumed);
238 self.shared
239 .ctx
240 .state
241 .update_connection_stats(
242 self.shared.ctx.server_id,
243 self.id,
244 Some(consumed as u64),
245 None,
246 Some(1),
247 None,
248 )
249 .await;
250 return Ok(Some((begin, message)));
251 }
252 Frame::Garbled { consumed, reason } => {
253 self.buf.drain(..consumed);
254 Log::new(Some(&self.shared.ctx.status_tx)).warn(format!(
255 "FIX connection {}: ignored garbled input ({reason})",
256 self.id
257 ));
258 }
259 Frame::Incomplete => {
260 anyhow::ensure!(
261 self.buf.len() <= codec::MAX_BODY + 64,
262 "unframed input over {} bytes",
263 codec::MAX_BODY
264 );
265 let mut chunk = [0u8; 8192];
266 let n = self.r.read(&mut chunk).await?;
267 if n == 0 {
268 return Ok(None);
269 }
270 self.buf.extend_from_slice(&chunk[..n]);
271 }
272 }
273 }
274 }
275}
276
277async fn connection(shared: &Shared, id: ConnectionId, stream: TcpStream) -> Result<()> {
278 let ctx = &shared.ctx;
279 let (r, w) = tokio::io::split(stream);
280 let mut c = Conn {
281 shared,
282 id,
283 r,
284 w,
285 buf: Vec::new(),
286 };
287 let Ok(first) = tokio::time::timeout(shared.logon_timeout, c.next()).await else {
289 outcome(ctx, id, "logon", "protocol_refusal");
290 return Ok(());
291 };
292 let Some((begin, logon)) = first? else {
293 return Ok(());
294 };
295 if logon.msg_type() != "A" || begin != shared.begin {
296 outcome(ctx, id, "logon", "protocol_refusal");
297 anyhow::bail!(
298 "the first message was {} {}, not a {} Logon; disconnected",
299 begin,
300 logon.msg_type(),
301 shared.begin
302 );
303 }
304 let peer = logon.get(49).unwrap_or_default().to_owned();
305 let heartbeat = logon
306 .get(108)
307 .and_then(|v| v.parse::<u64>().ok())
308 .filter(|h| *h <= 3600);
309 let refusal = if !comp_id_ok(&peer) {
310 Some("SenderCompID missing or invalid")
311 } else if logon.get(56) != Some(shared.comp_id.as_str()) {
312 Some("TargetCompID is not this acceptor")
313 } else if heartbeat.is_none() {
314 Some("HeartBtInt must be 0 to 3600")
315 } else if logon.get(98).unwrap_or("0") != "0" {
316 Some("only EncryptMethod 0 is supported")
317 } else if logon.seq().is_none() {
318 Some("MsgSeqNum missing")
319 } else {
320 None
321 };
322 let hb = heartbeat.unwrap_or(30);
323 let mut session = Session::new(
324 &shared.begin,
325 &shared.comp_id,
326 if peer.is_empty() { "UNKNOWN" } else { &peer },
327 if hb == 0 {
328 NO_HEARTBEAT
329 } else {
330 Duration::from_secs(hb)
331 },
332 );
333 if let Some(text) = refusal {
334 outcome(ctx, id, "logon", "protocol_refusal");
335 let bytes = session.logout(Some(text));
336 c.write(&[bytes]).await?;
337 return Ok(());
338 }
339 let reset = logon.get(141) == Some("Y");
340 let event = Event::new(
341 &actions::LOGON_EVENT,
342 json!({"sender_comp_id": peer, "target_comp_id": shared.comp_id, "heartbeat_secs": hb, "reset_seq_num": reset, "username": logon.get(553), "password": logon.get(554)}),
343 );
344 match ask(shared, id, event, "logon")
345 .await
346 .map(|mut a| a.remove(0))
347 {
348 Ok(a) if a["type"] == "fix_accept_logon" => outcome(ctx, id, "logon", "model_answer"),
349 Ok(a) if a["type"] == "fix_reject_logon" => {
350 outcome(ctx, id, "logon", "model_reject");
351 let bytes = session.logout(a["text"].as_str());
352 c.write(&[bytes]).await?;
353 return Ok(());
354 }
355 other => {
356 if let Ok(a) = other {
357 outcome(ctx, id, "logon", "fail_closed_invalid_reply");
358 Log::new(Some(&ctx.status_tx)).warn(format!(
359 "FIX connection {id}: {} does not answer a Logon",
360 a["type"]
361 ));
362 }
363 let bytes = session.logout(Some("logon cannot be processed now"));
364 c.write(&[bytes]).await?;
365 return Ok(());
366 }
367 }
368 session.accept_logon(logon.seq().unwrap_or(1));
369 let mut reply = vec![(98, "0".to_owned()), (108, hb.to_string())];
370 if reset {
371 reply.push((141, "Y".into()));
372 }
373 let bytes = session.encode("A", &reply)?;
374 c.write(&[bytes]).await?;
375 let mut commands =
376 crate::server::peer_support::register_peer_channel(&ctx.state, ctx.server_id, id.as_u32())
377 .await;
378 session_loop(&mut c, &mut session, &mut commands).await
379}
380
381async fn session_loop(
382 c: &mut Conn<'_>,
383 session: &mut Session,
384 commands: &mut mpsc::Receiver<ClientCommand>,
385) -> Result<()> {
386 let ctx = &c.shared.ctx;
387 let id = c.id;
388 let mut tick = tokio::time::interval(Duration::from_secs(1));
389 let mut closing: Option<Instant> = None;
390 loop {
391 if closing.is_some_and(|t| t.elapsed() >= LOGOUT_WAIT) {
392 return Ok(());
393 }
394 enum Wake {
395 Message(Option<Message>),
396 Tick,
397 Command(Option<ClientCommand>),
398 }
399 let wake = tokio::select! {
400 m = c.next() => Wake::Message(m?.map(|(_, m)| m)),
401 _ = tick.tick() => Wake::Tick,
402 cmd = commands.recv(), if closing.is_none() => Wake::Command(cmd),
403 };
404 let message = match wake {
405 Wake::Message(Some(m)) => m,
406 Wake::Message(None) => return Ok(()),
407 Wake::Tick => {
408 let (send, close) = session.tick();
409 c.write(&send).await?;
410 if let Some(reason) = close {
411 Log::new(Some(&ctx.status_tx)).warn(format!("FIX connection {id}: {reason}"));
412 return Ok(());
413 }
414 continue;
415 }
416 Wake::Command(None) => continue,
417 Wake::Command(Some(cmd)) => {
418 let action = cmd.action.clone();
419 let outcome = injected(c, session, &action).await;
420 if session.logout_sent {
421 closing.get_or_insert_with(Instant::now);
422 }
423 ctx.state
424 .record_access_log(
425 crate::state::AccessLogOwner::Server(ctx.server_id.as_u32()),
426 "FIX",
427 Some(id.as_u32()),
428 "injected_action",
429 json!({"type": action["type"], "msg_type": action["msg_type"]}),
430 vec![serde_json::to_value(&outcome).unwrap_or(Value::Null)],
431 )
432 .await;
433 let _ = cmd.reply_tx.send(Ok(outcome));
434 continue;
435 }
436 };
437 let step = session.on_message(message);
438 c.write(&step.send).await?;
439 if let Some(reason) = step.close {
440 Log::new(Some(&ctx.status_tx)).info(format!("FIX connection {id}: {reason}"));
441 return Ok(());
442 }
443 let Some(app) = step.app else { continue };
444 if closing.is_some() {
445 continue;
446 }
447 let msg_type = app.msg_type().to_owned();
448 let seq = app.seq().unwrap_or(0);
449 let fields: Vec<Value> = session::body(&app)
450 .into_iter()
451 .map(|(tag, value)| json!({"tag": tag, "name": dict::field_name(tag), "value": value}))
452 .collect();
453 let event = Event::new(
454 &actions::MESSAGE_EVENT,
455 json!({"msg_type": msg_type, "msg_type_name": dict::message_name(&msg_type), "seq": seq, "sender_comp_id": session.target, "fields": fields}),
456 );
457 match ask(c.shared, id, event, "message").await {
458 Ok(answers) => {
459 outcome(
460 ctx,
461 id,
462 "message",
463 if answers.iter().any(|a| a["type"] == "fix_reject") {
464 "model_reject"
465 } else {
466 "model_answer"
467 },
468 );
469 for a in answers {
470 apply(c, session, &a, seq, &msg_type).await?;
471 }
472 if session.logout_sent {
473 closing.get_or_insert_with(Instant::now);
474 }
475 }
476 Err(e) => {
477 let text = crate::utils::WireFailure::classify(&e).text();
478 let bytes = business_reject(session, seq, &msg_type, 4, text)?;
479 c.write(&[bytes]).await?;
480 }
481 }
482 }
483}
484
485fn business_reject(
486 session: &mut Session,
487 ref_seq: u32,
488 ref_type: &str,
489 reason: u32,
490 text: &str,
491) -> Result<Vec<u8>> {
492 session.encode(
493 "j",
494 &[
495 (45, ref_seq.to_string()),
496 (372, ref_type.to_owned()),
497 (380, reason.to_string()),
498 (58, text.to_owned()),
499 ],
500 )
501}
502
503async fn apply(
505 c: &mut Conn<'_>,
506 session: &mut Session,
507 a: &Value,
508 ref_seq: u32,
509 ref_type: &str,
510) -> Result<()> {
511 let bytes = match a["type"].as_str() {
512 Some("fix_send") => {
513 let msg_type = actions::app_msg_type(a)?;
514 session.encode(&msg_type, &actions::body_fields(a)?)?
515 }
516 Some("fix_reject") => {
517 let reason = actions::BUSINESS_REJECT
518 .iter()
519 .find(|(n, _)| a["reason"] == *n)
520 .map(|(_, c)| *c)
521 .unwrap_or(0);
522 business_reject(
523 session,
524 ref_seq,
525 ref_type,
526 reason,
527 a["text"].as_str().unwrap_or_default(),
528 )?
529 }
530 Some("fix_logout") => session.logout(a["text"].as_str()),
531 _ => return Ok(()),
532 };
533 c.write(&[bytes]).await
534}
535
536async fn injected(c: &mut Conn<'_>, session: &mut Session, a: &Value) -> ClientSendOutcome {
538 let checked = match a["type"].as_str() {
539 Some("fix_send" | "fix_logout") => actions::check_answer(a),
540 Some("disconnect") => Ok(()),
541 _ => Err(anyhow::anyhow!(
542 "a FIX session accepts fix_send, fix_logout or disconnect"
543 )),
544 };
545 if let Err(e) = checked {
546 return ClientSendOutcome::Rejected {
547 error: e.to_string(),
548 };
549 }
550 let bytes = match a["type"].as_str() {
551 Some("fix_send") => match actions::app_msg_type(a)
552 .and_then(|t| session.encode(&t, &actions::body_fields(a)?))
553 {
554 Ok(b) => b,
555 Err(e) => {
556 return ClientSendOutcome::Rejected {
557 error: e.to_string(),
558 }
559 }
560 },
561 Some("fix_logout") => session.logout(a["text"].as_str()),
562 _ => session.logout(None),
563 };
564 let n = bytes.len();
565 match c.write(&[bytes]).await {
566 Ok(()) if a["type"] == "disconnect" => ClientSendOutcome::Disconnected,
567 Ok(()) => ClientSendOutcome::Sent { bytes_sent: n },
568 Err(e) => ClientSendOutcome::Rejected {
569 error: e.to_string(),
570 },
571 }
572}