iscsi_client_rs/state_machine/login/
common.rs1use std::{collections::HashMap, marker::PhantomData, sync::Arc};
6
7use anyhow::{Context, Result, anyhow, bail};
8use tokio_util::sync::CancellationToken;
9use tracing::{debug, warn};
10
11use crate::{
12 cfg::config::{Config, login_keys_operational},
13 client::client::ClientConnection,
14 models::{
15 common::HEADER_LEN,
16 data_fromat::PduResponse,
17 login::{common::Stage, response::LoginResponse},
18 },
19 state_machine::{
20 common::{StateMachine, StateMachineCtx, Transition},
21 login::{
22 login_chap::{ChapA, ChapAnswer, ChapOpToFull, ChapSecurity},
23 login_plain::PlainStart,
24 },
25 },
26};
27
28#[derive(Debug)]
30pub struct LoginCtx<'a> {
31 _lt: PhantomData<&'a ()>,
32
33 pub conn: Arc<ClientConnection>,
35 pub isid: [u8; 6],
37 pub cid: u16,
39 pub tsih: u16,
41 pub itt: u32,
43 pub buf: [u8; HEADER_LEN],
45
46 pub last_response: Option<PduResponse<LoginResponse>>,
48
49 state: Option<LoginStates>,
50}
51
52impl<'a> LoginCtx<'a> {
53 pub fn new(conn: Arc<ClientConnection>, isid: [u8; 6], cid: u16, tsih: u16) -> Self {
55 Self {
56 conn,
57 isid,
58 cid,
59 tsih,
60 itt: 0,
61 buf: [0u8; HEADER_LEN],
62 last_response: None,
63 state: None,
64 _lt: PhantomData,
65 }
66 }
67
68 pub fn set_plain_login(&mut self) {
70 self.state = Some(LoginStates::PlainStart(PlainStart));
71 }
72
73 pub fn set_chap_login(&mut self) {
75 self.state = Some(LoginStates::ChapSecurity(ChapSecurity));
76 }
77
78 pub fn validate_last_response_header(&self) -> Result<&LoginResponse> {
80 match &self.last_response {
81 Some(l) => match l.header_view() {
82 Ok(last) => Ok(last),
83 Err(e) => Err(e),
84 },
85 None => Err(anyhow!("no last response in ctx")),
86 }
87 }
88
89 pub fn validate_last_response_pdu(&self) -> Result<&PduResponse<LoginResponse>> {
91 match &self.last_response {
92 Some(l) => Ok(l),
93 None => Err(anyhow!("no last response in ctx")),
94 }
95 }
96}
97
98pub type LoginStepOut = Transition<LoginStates, Result<()>>;
100
101#[derive(Debug)]
103pub enum LoginStates {
104 PlainStart(PlainStart),
106 ChapSecurity(ChapSecurity),
108 ChapA(ChapA),
110 ChapAnswer(ChapAnswer),
112 ChapOpToFull(ChapOpToFull),
114}
115
116impl<'ctx> StateMachineCtx<LoginCtx<'ctx>, PduResponse<LoginResponse>>
117 for LoginCtx<'ctx>
118{
119 async fn execute(
120 &mut self,
121 _cancel: &CancellationToken,
122 ) -> Result<PduResponse<LoginResponse>> {
123 debug!("Loop login");
124 loop {
125 let state = self.state.take().context("state must be set LoginCtx")?;
126 let tr = match state {
127 LoginStates::PlainStart(s) => s.step(self).await,
128 LoginStates::ChapSecurity(s) => s.step(self).await,
129 LoginStates::ChapA(s) => s.step(self).await,
130 LoginStates::ChapAnswer(s) => s.step(self).await,
131 LoginStates::ChapOpToFull(s) => s.step(self).await,
132 };
133
134 match tr {
135 Transition::Next(next_state, _r) => {
136 self.state = Some(next_state);
137 },
138 Transition::Stay(Ok(_)) => {},
139 Transition::Stay(Err(e)) => return Err(e),
140 Transition::Done(r) => {
141 r?;
142 return self
143 .last_response
144 .take()
145 .ok_or_else(|| anyhow!("no last response in ctx"));
146 },
147 }
148 }
149 }
150}
151
152fn parse_login_text_map(data: &[u8]) -> Result<HashMap<String, Vec<String>>> {
153 let mut map: HashMap<String, Vec<String>> = HashMap::new();
154 for entry in data.split(|b| *b == 0) {
155 if entry.is_empty() {
156 continue;
157 }
158 let entry_str = std::str::from_utf8(entry)
159 .context("login response contains invalid UTF-8 text")?;
160 let (key, value) = entry_str.split_once('=').ok_or_else(|| {
161 anyhow!("login response entry '{entry_str}' is missing '=' separator")
162 })?;
163 let value = value.trim().to_string();
164 if value == "NotUnderstood" {
165 warn!("{}={}", key.to_string(), value);
166 continue;
167 }
168
169 map.entry(key.to_string())
170 .or_default()
171 .push(value.trim().to_string());
172 }
173 Ok(map)
174}
175
176pub(crate) fn verify_operational_negotiation(
179 cfg: &Config,
180 rsp: &PduResponse<LoginResponse>,
181) -> Result<()> {
182 let header = rsp.header_view()?;
183 match header.flags.nsg() {
184 Some(Stage::FullFeature) => {},
185 other => bail!(
186 "login response NSG={other:?} (expected {:?}), status={:?}/{:?}",
187 Stage::FullFeature,
188 header.status_class,
189 header.status_detail,
190 ),
191 }
192
193 let data = rsp
194 .data()
195 .context("login response missing negotiation payload")?;
196 if data.is_empty() {
197 bail!("login response negotiation payload is empty");
198 }
199
200 let server = parse_login_text_map(data)?;
201 let expected_bytes = login_keys_operational(cfg);
202 let expected = parse_login_text_map(expected_bytes.as_slice())?;
203
204 let mut mismatches = Vec::new();
205 for (key, exp_values) in expected {
206 for expected_value in exp_values {
207 match server.get(&key) {
208 Some(values) if values.iter().any(|v| v == &expected_value) => {},
209 Some(values) => mismatches.push(format!(
210 "{key}: expected '{expected_value}' but target replied '{}'",
211 values.join(", ")
212 )),
213 None => {},
214 }
215 }
216 }
217
218 if mismatches.is_empty() {
219 Ok(())
220 } else {
221 bail!("login negotiation mismatch: {}", mismatches.join("; "))
222 }
223}