1use crate::server::ics_support::{self as io, number, DeviceSession, ScannerSession};
3use anyhow::{bail, ensure, Context, Result};
4use serde_json::{json, Value};
5use tokio::{io::AsyncWriteExt, net::TcpStream};
6
7pub const MAX_FRAME: usize = 487;
8pub const MAX_ITEMS: usize = 16;
9pub fn tpkt(data: &[u8]) -> Vec<u8> {
10 let len = u16::try_from(data.len() + 4).expect("bounded TPKT");
11 let mut out = vec![3, 0];
12 out.extend(len.to_be_bytes());
13 out.extend(data);
14 out
15}
16pub async fn read(stream: &mut TcpStream) -> Result<Vec<u8>> {
17 io::frame(stream, 4, MAX_FRAME, |h| {
18 ensure!(h[0..2] == [3, 0], "invalid TPKT version/reserved");
19 Ok(u16::from_be_bytes([h[2], h[3]]) as usize)
20 })
21 .await
22}
23fn s7(reference: u16, parameters: &[u8], data: &[u8], response: bool) -> Vec<u8> {
24 let mut p = vec![2, 0xf0, 0x80, 0x32, if response { 3 } else { 1 }, 0, 0];
25 p.extend(reference.to_be_bytes());
26 p.extend(
27 u16::try_from(parameters.len())
28 .expect("bounded params")
29 .to_be_bytes(),
30 );
31 p.extend(
32 u16::try_from(data.len())
33 .expect("bounded data")
34 .to_be_bytes(),
35 );
36 if response {
37 p.extend([0, 0]);
38 }
39 p.extend(parameters);
40 p.extend(data);
41 tpkt(&p)
42}
43fn parts(frame: &[u8], response: bool) -> Result<(u16, &[u8], &[u8])> {
44 ensure!(
45 frame.len() >= if response { 19 } else { 17 },
46 "short S7 header"
47 );
48 ensure!(
49 frame[4..7] == [2, 0xf0, 0x80]
50 && frame[7] == 0x32
51 && frame[8] == if response { 3 } else { 1 },
52 "unsupported COTP/S7 PDU"
53 );
54 ensure!(frame[9..11] == [0, 0], "S7 reserved field");
55 let pl = u16::from_be_bytes([frame[13], frame[14]]) as usize;
56 let dl = u16::from_be_bytes([frame[15], frame[16]]) as usize;
57 let start = if response {
58 ensure!(frame[17..19] == [0, 0], "S7 header error");
59 19
60 } else {
61 17
62 };
63 ensure!(start + pl + dl == frame.len(), "S7 lengths disagree");
64 Ok((
65 u16::from_be_bytes([frame[11], frame[12]]),
66 &frame[start..start + pl],
67 &frame[start + pl..],
68 ))
69}
70fn area_name(n: u8) -> Result<&'static str> {
71 match n {
72 0x81 => Ok("inputs"),
73 0x82 => Ok("outputs"),
74 0x83 => Ok("markers"),
75 0x84 => Ok("db"),
76 _ => bail!("unsupported data area"),
77 }
78}
79fn area_id(n: &str) -> Result<u8> {
80 match n {
81 "inputs" => Ok(0x81),
82 "outputs" => Ok(0x82),
83 "markers" => Ok(0x83),
84 "db" => Ok(0x84),
85 _ => bail!("unknown data area"),
86 }
87}
88
89#[derive(Default)]
90pub struct Device {
91 cotp: bool,
92 setup: bool,
93 pdu: usize,
94}
95#[async_trait::async_trait]
96impl DeviceSession for Device {
97 async fn read(&mut self, s: &mut TcpStream) -> Result<Vec<u8>> {
98 read(s).await
99 }
100 fn receive(&mut self, f: &[u8]) -> Result<(Vec<u8>, Option<Value>)> {
101 if !self.cotp {
102 ensure!(
103 f.len() >= 11 && f[5] == 0xe0 && f[4] as usize + 5 == f.len(),
104 "expected COTP connect"
105 );
106 ensure!(f[10] == 0, "unsupported COTP class");
107 let mut pos = 11;
108 while pos < f.len() {
109 ensure!(pos + 2 <= f.len(), "short COTP option");
110 let n = f[pos + 1] as usize;
111 ensure!(pos + 2 + n <= f.len(), "short COTP option value");
112 pos += 2 + n;
113 }
114 let mut cc = f[4..].to_vec();
115 cc[1] = 0xd0;
116 cc[2..4].copy_from_slice(&f[8..10]);
117 cc[4..6].copy_from_slice(&[0, 1]);
118 self.cotp = true;
119 return Ok((tpkt(&cc), None));
120 }
121 let (reference, p, d) = parts(f, false)?;
122 ensure!(!p.is_empty(), "missing S7 function");
123 if p[0] == 0xf0 {
124 ensure!(
125 p.len() == 8 && d.is_empty() && p[1] == 0,
126 "invalid setup communication"
127 );
128 let offered = u16::from_be_bytes([p[6], p[7]]) as usize;
129 ensure!(offered >= 240, "PDU too small");
130 self.pdu = offered.min(480);
131 self.setup = true;
132 let mut answer = vec![0xf0, 0, 0, 1, 0, 1];
133 answer.extend((self.pdu as u16).to_be_bytes());
134 return Ok((s7(reference, &answer, &[], true), None));
135 }
136 ensure!(
137 self.setup && f.len() <= self.pdu + 7,
138 "S7 setup required or negotiated PDU exceeded"
139 );
140 if !matches!(p[0], 4 | 5) {
141 return Ok((
142 s7(reference, &[p[0]], &[], true)
143 .into_iter()
144 .enumerate()
145 .map(|(i, b)| {
146 if i == 17 {
147 0x81
148 } else if i == 18 {
149 0x04
150 } else {
151 b
152 }
153 })
154 .collect(),
155 None,
156 ));
157 }
158 ensure!(p.len() >= 2, "short S7 item count");
159 let count = p[1] as usize;
160 ensure!(
161 (1..=MAX_ITEMS).contains(&count) && p.len() == 2 + 12 * count,
162 "invalid S7 item list"
163 );
164 let mut items = Vec::new();
165 let mut offset = 0;
166 for item in p[2..].chunks_exact(12) {
167 ensure!(
168 item[..4] == [0x12, 10, 0x10, 2],
169 "only S7ANY byte reads/writes are supported"
170 );
171 let amount = u16::from_be_bytes([item[4], item[5]]) as usize;
172 ensure!((1..=200).contains(&amount), "item length outside 1..200");
173 let db = u16::from_be_bytes([item[6], item[7]]);
174 let area = area_name(item[8])?;
175 let bit_address = ((item[9] as u32) << 16) | ((item[10] as u32) << 8) | item[11] as u32;
176 ensure!(bit_address % 8 == 0, "byte address must be aligned");
177 ensure!(area == "db" || db == 0, "non-DB area has DB number");
178 let mut v = json!({"area":area,"db":db,"start":bit_address/8,"count":amount});
179 if p[0] == 5 {
180 ensure!(
181 offset + 4 <= d.len() && d[offset] == 0 && d[offset + 1] == 4,
182 "invalid write data header"
183 );
184 let bits = u16::from_be_bytes([d[offset + 2], d[offset + 3]]) as usize;
185 ensure!(
186 bits == amount * 8 && offset + 4 + amount <= d.len(),
187 "write data length disagrees"
188 );
189 v["values"] = json!(d[offset + 4..offset + 4 + amount]);
190 offset += 4 + amount;
191 if amount % 2 == 1 && items.len() + 1 < count {
192 ensure!(d.get(offset) == Some(&0), "missing item padding");
193 offset += 1;
194 }
195 }
196 items.push(v);
197 }
198 ensure!(offset == d.len(), "unexpected S7 job data");
199 Ok((
200 vec![],
201 Some(
202 json!({"operation":if p[0]==4{"read"}else{"write"},"reference":reference,"items":items}),
203 ),
204 ))
205 }
206 fn answer(&mut self, r: &Value, a: Option<&Value>) -> Result<Vec<u8>> {
207 let items = r["items"].as_array().context("items")?;
208 let read = r["operation"] == "read";
209 let answers = a
210 .filter(|a| a["type"] == "s7comm_reply")
211 .and_then(|a| a["items"].as_array())
212 .filter(|a| a.len() == items.len());
213 let mut data = vec![];
214 for (i, item) in items.iter().enumerate() {
215 let supplied = answers.and_then(|a| a.get(i));
216 let error = supplied.and_then(|v| v["error"].as_str());
217 let code = match error {
218 Some("address") => 5,
219 Some("unsupported") => 6,
220 Some("denied") => 3,
221 Some(_) => 10,
222 None => 0xff,
223 };
224 if read {
225 let vals = supplied
226 .and_then(|v| v["values"].as_array())
227 .filter(|v| v.len() == item["count"].as_u64().unwrap_or(0) as usize);
228 let valid = code == 0xff
229 && vals.is_some_and(|v| v.iter().all(|n| n.as_u64().is_some_and(|n| n <= 255)));
230 if valid {
231 let vals = vals.expect("checked");
232 data.extend([0xff, 4]);
233 data.extend((u16::try_from(vals.len() * 8)?).to_be_bytes());
234 data.extend(vals.iter().map(|n| n.as_u64().expect("checked") as u8));
235 if vals.len() % 2 == 1 && i + 1 < items.len() {
236 data.push(0);
237 }
238 } else {
239 data.extend([if code == 0xff { 3 } else { code }, 0, 0, 0]);
240 }
241 } else {
242 data.push(
243 if supplied.is_some_and(|v| v["accepted"] == true) && error.is_none() {
244 0xff
245 } else if code == 0xff {
246 3
247 } else {
248 code
249 },
250 );
251 }
252 }
253 ensure!(
254 data.len() + 14 <= self.pdu,
255 "response exceeds negotiated PDU"
256 );
257 Ok(s7(
258 u16::try_from(number(r, "reference", 65535)?)?,
259 &[if read { 4 } else { 5 }, items.len() as u8],
260 &data,
261 true,
262 ))
263 }
264}
265
266pub fn validate(a: &Value) -> Result<()> {
267 ensure!(
268 matches!(a["type"].as_str(), Some("s7comm_read" | "s7comm_write")),
269 "unknown S7comm action"
270 );
271 area_id(a["area"].as_str().context("area required")?)?;
272 number(a, "db", 65535)?;
273 number(a, "start", 0x1fffff)?;
274 if a["type"] == "s7comm_read" {
275 let c = number(a, "count", 200)?;
276 ensure!(c > 0, "count must be positive");
277 } else {
278 let values = a["values"].as_array().context("values required")?;
279 ensure!(
280 !values.is_empty() && values.len() <= 200,
281 "write count outside 1..200"
282 );
283 for n in values {
284 ensure!(n.as_u64().is_some_and(|n| n <= 255), "values must be bytes");
285 }
286 }
287 ensure!(
288 a["area"] == "db" || a["db"] == 0,
289 "non-DB area must have db=0"
290 );
291 Ok(())
292}
293#[derive(Default)]
294pub struct Scanner {
295 reference: u16,
296}
297#[async_trait::async_trait]
298impl ScannerSession for Scanner {
299 async fn open(&mut self, s: &mut TcpStream) -> Result<()> {
300 s.write_all(&tpkt(&[
301 17, 0xe0, 0, 0, 0, 1, 0, 0xc0, 1, 10, 0xc1, 2, 1, 0, 0xc2, 2, 1, 2,
302 ]))
303 .await?;
304 let cc = read(s).await?;
305 ensure!(
306 cc.len() >= 11 && cc[5] == 0xd0 && cc[6..8] == [0, 1],
307 "COTP connection refused"
308 );
309 s.write_all(&s7(1, &[0xf0, 0, 0, 1, 0, 1, 1, 0xe0], &[], false))
310 .await?;
311 let f = read(s).await?;
312 let (reference, p, d) = parts(&f, true)?;
313 ensure!(
314 reference == 1 && p.len() == 8 && p[0] == 0xf0 && d.is_empty(),
315 "invalid setup response"
316 );
317 self.reference = 1;
318 Ok(())
319 }
320 async fn exchange(&mut self, s: &mut TcpStream, a: &Value) -> Result<Value> {
321 validate(a)?;
322 self.reference = self.reference.wrapping_add(1);
323 let write = a["type"] == "s7comm_write";
324 let count = if write {
325 a["values"].as_array().context("values")?.len()
326 } else {
327 number(a, "count", 200)? as usize
328 };
329 let mut p = vec![if write { 5 } else { 4 }, 1, 0x12, 10, 0x10, 2];
330 p.extend((count as u16).to_be_bytes());
331 p.extend((number(a, "db", 65535)? as u16).to_be_bytes());
332 p.push(area_id(a["area"].as_str().context("area")?)?);
333 let bits = (number(a, "start", 0x1fffff)? as u32) * 8;
334 p.extend(&bits.to_be_bytes()[1..]);
335 let mut data = vec![];
336 if write {
337 data.extend([0, 4]);
338 data.extend(((count * 8) as u16).to_be_bytes());
339 data.extend(
340 a["values"]
341 .as_array()
342 .context("values")?
343 .iter()
344 .map(|v| v.as_u64().expect("validated") as u8),
345 );
346 }
347 s.write_all(&s7(self.reference, &p, &data, false)).await?;
348 let f = read(s).await?;
349 let (reference, p, d) = parts(&f, true)?;
350 ensure!(
351 reference == self.reference && p == [if write { 5 } else { 4 }, 1],
352 "S7 response correlation failed"
353 );
354 ensure!(!d.is_empty(), "missing item status");
355 if d[0] != 0xff {
356 ensure!(d.len() == if write { 1 } else { 4 }, "bad error item");
357 return Ok(
358 json!({"operation":if write{"write"}else{"read"},"status":d[0],"success":false}),
359 );
360 }
361 if write {
362 ensure!(d.len() == 1, "write response length");
363 Ok(json!({"operation":"write","success":true}))
364 } else {
365 ensure!(
366 d.len() == count + 4
367 && d[1] == 4
368 && u16::from_be_bytes([d[2], d[3]]) as usize == count * 8,
369 "read response shape"
370 );
371 Ok(json!({"operation":"read","success":true,"values":&d[4..]}))
372 }
373 }
374}