Skip to main content

netget/server/s7comm/
codec.rs

1//! Selected ISO-on-TCP S7comm jobs. Values are byte-sized data-area elements.
2use 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}