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};
6pub const MAX_FRAME: usize = 4096;
7pub const PORT: u16 = 44818;
8fn u16le(b: &[u8]) -> u16 {
9 u16::from_le_bytes([b[0], b[1]])
10}
11fn u32le(b: &[u8]) -> u32 {
12 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
13}
14pub fn encapsulate(
15 command: u16,
16 session: u32,
17 context: [u8; 8],
18 status: u32,
19 data: &[u8],
20) -> Vec<u8> {
21 let mut p = vec![];
22 p.extend(command.to_le_bytes());
23 p.extend(
24 u16::try_from(data.len())
25 .expect("bounded payload")
26 .to_le_bytes(),
27 );
28 p.extend(session.to_le_bytes());
29 p.extend(status.to_le_bytes());
30 p.extend(context);
31 p.extend([0; 4]);
32 p.extend(data);
33 p
34}
35pub async fn read(s: &mut TcpStream) -> Result<Vec<u8>> {
36 io::frame(s, 24, MAX_FRAME, |h| {
37 ensure!(h[20..24] == [0; 4], "unsupported encapsulation options");
38 Ok(24 + u16le(&h[2..]) as usize)
39 })
40 .await
41}
42fn context(f: &[u8]) -> [u8; 8] {
43 f[12..20].try_into().expect("checked header")
44}
45pub fn identity(addr: std::net::SocketAddr) -> Vec<u8> {
47 let mut data = vec![1, 0];
48 data.extend(2u16.to_be_bytes());
49 data.extend(addr.port().to_be_bytes());
50 data.extend(match addr.ip() {
51 std::net::IpAddr::V4(v) => v.octets(),
52 _ => [127, 0, 0, 1],
53 });
54 data.extend([0; 8]);
55 data.extend(0u16.to_le_bytes());
56 data.extend(0u16.to_le_bytes());
57 data.extend(1u16.to_le_bytes());
58 data.extend([1, 0]);
59 data.extend(0u16.to_le_bytes());
60 data.extend(1u32.to_le_bytes());
61 let name = b"NetGet simulator";
62 data.push(name.len() as u8);
63 data.extend(name);
64 data.push(3);
65 let mut out = vec![1, 0, 0x0c, 0];
66 out.extend((data.len() as u16).to_le_bytes());
67 out.extend(data);
68 out
69}
70fn cpf(cip: &[u8]) -> Vec<u8> {
71 let mut p = vec![0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0xb2, 0];
72 p.extend((cip.len() as u16).to_le_bytes());
73 p.extend(cip);
74 p
75}
76fn cip_payload(p: &[u8]) -> Result<&[u8]> {
77 ensure!(
78 p.len() >= 16
79 && p[..4] == [0; 4]
80 && u16le(&p[6..]) == 2
81 && p[8..12] == [0; 4]
82 && p[12..14] == [0xb2, 0],
83 "unsupported CPF/interface"
84 );
85 let n = u16le(&p[14..]) as usize;
86 ensure!(p.len() == 16 + n, "CPF length mismatch");
87 Ok(&p[16..])
88}
89fn path(cip: &[u8]) -> Result<(u16, u16, Option<u16>, &[u8])> {
90 ensure!(cip.len() >= 2, "short CIP request");
91 let end = 2 + cip[1] as usize * 2;
92 ensure!(end <= cip.len(), "short CIP path");
93 let mut pos = 2;
94 let mut c = None;
95 let mut i = None;
96 let mut a = None;
97 while pos < end {
98 let code = cip[pos];
99 let (n, used) = match code {
100 0x20 | 0x24 | 0x30 => {
101 ensure!(pos + 2 <= end, "short logical segment");
102 (cip[pos + 1] as u16, 2)
103 }
104 0x21 | 0x25 | 0x31 => {
105 ensure!(
106 pos + 4 <= end && cip[pos + 1] == 0,
107 "short/padded logical segment"
108 );
109 (u16le(&cip[pos + 2..]), 4)
110 }
111 _ => bail!("unsupported CIP path segment"),
112 };
113 let slot = match code & 0xfc {
114 0x20 => &mut c,
115 0x24 => &mut i,
116 0x30 => &mut a,
117 _ => unreachable!(),
118 };
119 ensure!(slot.is_none(), "duplicate CIP path segment");
120 *slot = Some(n);
121 pos += used;
122 }
123 Ok((
124 c.context("class missing")?,
125 i.context("instance missing")?,
126 a,
127 &cip[end..],
128 ))
129}
130fn cip_error(service: u8, status: u8) -> Vec<u8> {
131 vec![service | 0x80, 0, status, 0]
132}
133fn encode_value(v: &Value) -> Result<Vec<u8>> {
134 let kind = v["value_type"].as_str().context("value_type required")?;
135 Ok(match kind {
136 "uint8" => vec![number(v, "value", 255)? as u8],
137 "uint16" => (number(v, "value", 65535)? as u16).to_le_bytes().to_vec(),
138 "uint32" => (number(v, "value", u32::MAX as u64)? as u32)
139 .to_le_bytes()
140 .to_vec(),
141 "int32" => {
142 let n = v["value"].as_i64().context("signed value")?;
143 i32::try_from(n)?.to_le_bytes().to_vec()
144 }
145 "real" => {
146 let n = v["value"].as_f64().context("real value")?;
147 ensure!(n.is_finite() && (n as f32).is_finite(), "nonfinite value");
148 (n as f32).to_le_bytes().to_vec()
149 }
150 "string" => {
151 let t = v["value"].as_str().context("string value")?;
152 ensure!(
153 t.is_ascii() && t.len() <= 255,
154 "short ASCII string required"
155 );
156 let mut b = vec![t.len() as u8];
157 b.extend(t.as_bytes());
158 b
159 }
160 _ => bail!("unsupported attribute value_type"),
161 })
162}
163fn decode_value(kind: &str, b: &[u8]) -> Result<Value> {
164 Ok(match kind {
165 "uint8" => {
166 ensure!(b.len() == 1, "uint8 length");
167 json!(b[0])
168 }
169 "uint16" => {
170 ensure!(b.len() == 2, "uint16 length");
171 json!(u16le(b))
172 }
173 "uint32" => {
174 ensure!(b.len() == 4, "uint32 length");
175 json!(u32le(b))
176 }
177 "int32" => {
178 ensure!(b.len() == 4, "int32 length");
179 json!(i32::from_le_bytes(b.try_into()?))
180 }
181 "real" => {
182 ensure!(b.len() == 4, "real length");
183 let n = f32::from_le_bytes(b.try_into()?);
184 ensure!(n.is_finite(), "nonfinite real");
185 json!(n)
186 }
187 "string" => {
188 ensure!(
189 !b.is_empty() && b.len() == 1 + b[0] as usize,
190 "short string length"
191 );
192 json!(std::str::from_utf8(&b[1..])?)
193 }
194 _ => bail!("unsupported attribute value_type"),
195 })
196}
197pub struct Device {
198 session: u32,
199 addr: std::net::SocketAddr,
200 pub types: Vec<Value>,
201 context: [u8; 8],
202}
203impl Default for Device {
204 fn default() -> Self {
205 Self {
206 session: 0,
207 types: vec![],
208 context: [0; 8],
209 addr: "127.0.0.1:44818".parse().expect("constant"),
210 }
211 }
212}
213#[async_trait::async_trait]
214impl DeviceSession for Device {
215 async fn read(&mut self, s: &mut TcpStream) -> Result<Vec<u8>> {
216 self.addr = s.local_addr()?;
217 read(s).await
218 }
219 fn receive(&mut self, f: &[u8]) -> Result<(Vec<u8>, Option<Value>)> {
220 ensure!(
221 f.len() >= 24 && u32le(&f[8..]) == 0,
222 "invalid encapsulation header"
223 );
224 let command = u16le(f);
225 let session = u32le(&f[4..]);
226 let payload = &f[24..];
227 let ctx = context(f);
228 let auto = |status, data: &[u8]| encapsulate(command, session, ctx, status, data);
229 if matches!(command, 0x63 | 0x64 | 0x04) {
230 ensure!(
231 payload.is_empty() && session == 0,
232 "discovery fields must be zero"
233 );
234 let data = match command {
235 0x63 => identity(self.addr),
236 0x64 => vec![0, 0],
237 _ => {
238 let mut p = vec![1, 0, 0, 1, 20, 0, 1, 0, 0x20, 0];
239 p.extend(b"Communications\0\0");
240 p
241 }
242 };
243 return Ok((auto(0, &data), None));
244 }
245 if command == 0x65 {
246 if payload != [1, 0, 0, 0] || session != 0 {
247 return Ok((auto(0x69, &[]), None));
248 }
249 static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(1);
250 self.session = NEXT
251 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
252 .max(1);
253 return Ok((encapsulate(command, self.session, ctx, 0, payload), None));
254 }
255 if self.session == 0 || session != self.session {
256 return Ok((auto(0x64, &[]), None));
257 }
258 if command == 0x66 {
259 bail!("session unregistered");
260 }
261 if command != 0x6f {
262 return Ok((auto(1, &[]), None));
263 }
264 let cip = cip_payload(payload)?;
265 ensure!(!cip.is_empty(), "empty CIP");
266 let (c, i, a, body) = match path(cip) {
267 Ok(p) => p,
268 Err(_) => return Ok((auto(0, &cpf(&cip_error(cip[0], 4))), None)),
269 };
270 if !matches!(cip[0], 0x0e | 0x10) {
271 return Ok((auto(0, &cpf(&cip_error(cip[0], 8))), None));
272 }
273 let Some(attribute) = a else {
274 return Ok((auto(0, &cpf(&cip_error(cip[0], 4))), None));
275 };
276 if cip[0] == 0x0e {
277 ensure!(body.is_empty(), "unexpected GetAttributeSingle data");
278 }
279 let mut request = json!({"operation":if cip[0]==0x0e{"get"}else{"set"},"class":c,"instance":i,"attribute":attribute,"service":cip[0]});
280 self.context = ctx;
282 if cip[0] == 0x10 {
283 ensure!(body.len() <= 1024, "attribute too large");
284 let configured = self.types.iter().find(|a| {
285 a["class"] == c
286 && a["instance"] == i
287 && a["attribute"].as_u64() == Some(attribute as u64)
288 });
289 let kind = configured
290 .and_then(|t| t["value_type"].as_str())
291 .or(if c == 1 && i == 1 {
292 match attribute {
293 1..=5 => Some("uint16"),
294 6 => Some("uint32"),
295 7 => Some("string"),
296 8 => Some("uint8"),
297 _ => None,
298 }
299 } else {
300 None
301 });
302 let Some(kind) = kind else {
303 return Ok((auto(0, &cpf(&cip_error(cip[0], 0x14))), None));
304 };
305 let value = match decode_value(kind, body) {
306 Ok(v) => v,
307 Err(_) => return Ok((auto(0, &cpf(&cip_error(cip[0], 9))), None)),
308 };
309 request["value_type"] = json!(kind);
310 request["value"] = value;
311 }
312 Ok((vec![], Some(request)))
313 }
314 fn answer(&mut self, r: &Value, a: Option<&Value>) -> Result<Vec<u8>> {
315 let service = number(r, "service", 255)? as u8;
316 let a = a.filter(|a| a["type"] == "ethernet_ip_reply");
317 let mut cip = cip_error(service, 0x0f);
318 if let Some(a) = a {
319 if let Some(status) = a.get("status").filter(|v| !v.is_null()) {
320 let n = status.as_u64().context("status integer")?;
321 ensure!((1..=255).contains(&n), "error status 1..255");
322 cip = cip_error(service, n as u8);
323 } else if service == 0x0e {
324 match encode_value(a) {
325 Ok(value) => {
326 cip = cip_error(service, 0);
327 cip.extend(value)
328 }
329 Err(_) => {}
330 }
331 } else if a["accepted"] == true {
332 cip = cip_error(service, 0);
333 }
334 }
335 Ok(encapsulate(0x6f, self.session, self.context, 0, &cpf(&cip)))
336 }
337}
338pub fn validate(a: &Value) -> Result<()> {
339 if a["type"] == "ethernet_ip_discover" {
340 return Ok(());
341 }
342 ensure!(
343 matches!(
344 a["type"].as_str(),
345 Some("ethernet_ip_get" | "ethernet_ip_set")
346 ),
347 "unknown explicit messaging action"
348 );
349 for field in ["class", "instance", "attribute"] {
350 number(a, field, 65535)?;
351 }
352 let kind = a["value_type"].as_str().context("value_type required")?;
353 ensure!(
354 matches!(
355 kind,
356 "uint8" | "uint16" | "uint32" | "int32" | "real" | "string"
357 ),
358 "unsupported value_type"
359 );
360 if a["type"] == "ethernet_ip_set" {
361 encode_value(a)?;
362 }
363 Ok(())
364}
365#[derive(Default)]
366pub struct Scanner {
367 session: u32,
368 context: u64,
369}
370impl Scanner {
371 async fn send(&mut self, s: &mut TcpStream, command: u16, payload: &[u8]) -> Result<Vec<u8>> {
372 self.context = self.context.wrapping_add(1);
373 let ctx = self.context.to_le_bytes();
374 let session = if command == 0x63 { 0 } else { self.session };
375 s.write_all(&encapsulate(command, session, ctx, 0, payload))
376 .await?;
377 let f = read(s).await?;
378 ensure!(
379 u16le(&f) == command && context(&f) == ctx,
380 "encapsulation response correlation failed"
381 );
382 ensure!(
383 u32le(&f[8..]) == 0,
384 "encapsulation error {}",
385 u32le(&f[8..])
386 );
387 if command != 0x65 {
388 ensure!(u32le(&f[4..]) == session, "session mismatch");
389 }
390 Ok(f)
391 }
392}
393#[async_trait::async_trait]
394impl ScannerSession for Scanner {
395 async fn open(&mut self, s: &mut TcpStream) -> Result<()> {
396 let f = self.send(s, 0x65, &[1, 0, 0, 0]).await?;
397 ensure!(f[24..] == [1, 0, 0, 0], "registration payload invalid");
398 self.session = u32le(&f[4..]);
399 ensure!(self.session != 0, "zero session handle");
400 Ok(())
401 }
402 async fn exchange(&mut self, s: &mut TcpStream, a: &Value) -> Result<Value> {
403 validate(a)?;
404 if a["type"] == "ethernet_ip_discover" {
405 let f = self.send(s, 0x63, &[]).await?;
406 let p = &f[24..];
407 ensure!(
408 p.len() >= 39
409 && u16le(p) == 1
410 && u16le(&p[2..]) == 0x0c
411 && u16le(&p[4..]) as usize + 6 == p.len(),
412 "invalid identity list"
413 );
414 let v = &p[6..];
415 let len = v[32] as usize;
416 ensure!(v.len() == 34 + len, "identity name length");
417 return Ok(
418 json!({"vendor":u16le(&v[18..]),"device_type":u16le(&v[20..]),"product":u16le(&v[22..]),"serial":u32le(&v[28..]),"name":std::str::from_utf8(&v[33..33+len])?}),
419 );
420 }
421 let service = if a["type"] == "ethernet_ip_get" {
422 0x0e
423 } else {
424 0x10
425 };
426 let mut path = vec![];
427 for (key, segment) in [("class", 0x20), ("instance", 0x24), ("attribute", 0x30)] {
428 let n = number(a, key, 65535)? as u16;
429 if n <= 255 {
430 path.extend([segment, n as u8]);
431 } else {
432 path.extend([segment + 1, 0]);
433 path.extend(n.to_le_bytes());
434 }
435 }
436 let mut cip = vec![service, (path.len() / 2) as u8];
437 cip.extend(path);
438 if service == 0x10 {
439 cip.extend(encode_value(a)?);
440 }
441 let f = self.send(s, 0x6f, &cpf(&cip)).await?;
442 let p = cip_payload(&f[24..])?;
443 ensure!(
444 p.len() >= 4 && p[0] == service | 0x80 && p[1] == 0 && p.len() >= 4 + 2 * p[3] as usize,
445 "CIP response header"
446 );
447 if p[2] != 0 {
448 return Ok(json!({"success":false,"status":p[2]}));
449 }
450 let body = &p[4 + 2 * p[3] as usize..];
451 if service == 0x10 {
452 ensure!(body.is_empty(), "unexpected write response data");
453 Ok(json!({"success":true}))
454 } else {
455 Ok(
456 json!({"success":true,"value":decode_value(a["value_type"].as_str().context("type")?,body)?}),
457 )
458 }
459 }
460}