Skip to main content

ironwork_exec/
tn3270.rs

1//! A `Terminal` over a TCP connection speaking TN3270 (RFC 1576): telnet negotiation of the
2//! terminal type, EOR and BINARY, then 3270 data streams as records ended by IAC EOR.
3
4use crate::cics::Terminal;
5use std::cell::RefCell;
6use std::collections::VecDeque;
7use std::io::{Read, Write};
8use std::net::TcpStream;
9use std::rc::Rc;
10use std::time::Duration;
11
12const IAC: u8 = 255;
13const DONT: u8 = 254;
14const DO: u8 = 253;
15const WONT: u8 = 252;
16const WILL: u8 = 251;
17const SB: u8 = 250;
18const SE: u8 = 240;
19const EOR_COMMAND: u8 = 239;
20
21const BINARY: u8 = 0;
22const TERMINAL_TYPE: u8 = 24;
23const EOR: u8 = 25;
24
25const TT_IS: u8 = 0;
26const TT_SEND: u8 = 1;
27
28/// How long a client may take over the negotiation before the connection is given up.
29const NEGOTIATION_TIMEOUT: Duration = Duration::from_secs(30);
30
31/// One unit of the telnet stream.
32enum Item {
33    Data(u8),
34    Verb(u8, u8),
35    Sub(Vec<u8>),
36    Eor,
37    Other,
38}
39
40/// What the client has agreed to so far.
41#[derive(Default)]
42struct Agreed {
43    terminal_type: bool,
44    eor_in: bool,
45    eor_out: bool,
46    binary_in: bool,
47    binary_out: bool,
48}
49
50impl Agreed {
51    fn options(&self) -> bool {
52        self.eor_in && self.eor_out && self.binary_in && self.binary_out
53    }
54}
55
56/// A connected 3270 terminal.
57#[derive(Debug)]
58pub struct Tn3270 {
59    stream: TcpStream,
60    buffer: Vec<u8>,
61    at: usize,
62    rows: usize,
63    columns: usize,
64    pub terminal_type: String,
65    pending: VecDeque<Vec<u8>>,
66}
67
68/// The screen size a terminal type names: IBM-327x-N with model N, an -E suffix changing nothing.
69pub fn screen_size(terminal_type: &str) -> (usize, usize) {
70    let upper = terminal_type.to_ascii_uppercase();
71    let model = upper.strip_prefix("IBM-3278-").or_else(|| upper.strip_prefix("IBM-3279-")).and_then(|m| m.chars().next());
72    match model {
73        Some('3') => (32, 80),
74        Some('4') => (43, 80),
75        Some('5') => (27, 132),
76        _ => (24, 80),
77    }
78}
79
80/// Negotiates the terminal type, EOR and BINARY with a freshly connected client.
81pub fn negotiate(stream: TcpStream) -> Result<Tn3270, String> {
82    let io = |e: std::io::Error| e.to_string();
83    stream.set_nodelay(true).map_err(io)?;
84    stream.set_read_timeout(Some(NEGOTIATION_TIMEOUT)).map_err(io)?;
85    let mut t = Tn3270 { stream, buffer: Vec::new(), at: 0, rows: 24, columns: 80, terminal_type: String::new(), pending: VecDeque::new() };
86    let mut agreed = Agreed::default();
87
88    t.write(&[IAC, DO, TERMINAL_TYPE])?;
89    while !agreed.terminal_type {
90        match t.expect_item()? {
91            Item::Verb(WONT, TERMINAL_TYPE) => return Err("the client refused TERMINAL-TYPE: this is not a 3270 emulator".into()),
92            Item::Verb(verb, option) => t.note(verb, option, &mut agreed)?,
93            _ => {}
94        }
95    }
96
97    t.write(&[IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, SE])?;
98    loop {
99        match t.expect_item()? {
100            Item::Sub(payload) if payload.starts_with(&[TERMINAL_TYPE, TT_IS]) => {
101                t.terminal_type = String::from_utf8_lossy(&payload[2..]).trim().to_ascii_uppercase();
102                break;
103            }
104            Item::Verb(verb, option) => t.note(verb, option, &mut agreed)?,
105            _ => {}
106        }
107    }
108    (t.rows, t.columns) = screen_size(&t.terminal_type);
109
110    t.write(&[IAC, DO, EOR, IAC, WILL, EOR, IAC, DO, BINARY, IAC, WILL, BINARY])?;
111    while !agreed.options() {
112        if let Item::Verb(verb, option) = t.expect_item()? {
113            t.note(verb, option, &mut agreed)?;
114        }
115    }
116    t.stream.set_read_timeout(None).map_err(io)?;
117    Ok(t)
118}
119
120impl Tn3270 {
121    /// Queues a record that the next `receive` returns before reading the connection.
122    pub fn push_back(&mut self, record: Vec<u8>) {
123        self.pending.push_back(record);
124    }
125
126    fn write(&mut self, bytes: &[u8]) -> Result<(), String> {
127        self.stream.write_all(bytes).and_then(|()| self.stream.flush()).map_err(|e| format!("writing to the terminal: {e}"))
128    }
129
130    /// The next byte of the connection, None at end of stream.
131    fn byte(&mut self) -> Result<Option<u8>, String> {
132        if self.at == self.buffer.len() {
133            self.buffer.resize(4096, 0);
134            self.at = 0;
135            let n = loop {
136                match self.stream.read(&mut self.buffer) {
137                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
138                    Err(e) => {
139                        self.buffer.clear();
140                        return Err(format!("reading from the terminal: {e}"));
141                    }
142                    Ok(n) => break n,
143                }
144            };
145            self.buffer.truncate(n);
146            if n == 0 {
147                return Ok(None);
148            }
149        }
150        self.at += 1;
151        Ok(Some(self.buffer[self.at - 1]))
152    }
153
154    fn need(&mut self) -> Result<u8, String> {
155        self.byte()?.ok_or_else(|| "the terminal closed the connection inside a telnet command".to_string())
156    }
157
158    /// The next data byte or telnet command; None at end of stream between items.
159    fn item(&mut self) -> Result<Option<Item>, String> {
160        let Some(b) = self.byte()? else { return Ok(None) };
161        if b != IAC {
162            return Ok(Some(Item::Data(b)));
163        }
164        Ok(Some(match self.need()? {
165            IAC => Item::Data(IAC),
166            EOR_COMMAND => Item::Eor,
167            verb @ (DO | DONT | WILL | WONT) => Item::Verb(verb, self.need()?),
168            SB => {
169                let mut payload = Vec::new();
170                loop {
171                    match self.need()? {
172                        IAC => match self.need()? {
173                            SE => break,
174                            IAC => payload.push(IAC),
175                            _ => {}
176                        },
177                        b => payload.push(b),
178                    }
179                }
180                Item::Sub(payload)
181            }
182            _ => Item::Other,
183        }))
184    }
185
186    fn expect_item(&mut self) -> Result<Item, String> {
187        self.item()?.ok_or_else(|| "the terminal closed the connection during negotiation".to_string())
188    }
189
190    /// Records the client's word on an option, and refuses any option we do not use.
191    fn note(&mut self, verb: u8, option: u8, agreed: &mut Agreed) -> Result<(), String> {
192        let flag = match (option, verb) {
193            (TERMINAL_TYPE, WILL) => Some(&mut agreed.terminal_type),
194            (EOR, WILL) => Some(&mut agreed.eor_in),
195            (EOR, DO) => Some(&mut agreed.eor_out),
196            (BINARY, WILL) => Some(&mut agreed.binary_in),
197            (BINARY, DO) => Some(&mut agreed.binary_out),
198            (EOR | BINARY, WONT | DONT) => return Err(format!("the client refused telnet option {option}: this is not a 3270 emulator")),
199            _ => None,
200        };
201        match flag {
202            Some(flag) => *flag = true,
203            None => self.refuse(verb, option)?,
204        }
205        Ok(())
206    }
207
208    /// Answers a request for an option we do not use.
209    fn refuse(&mut self, verb: u8, option: u8) -> Result<(), String> {
210        match verb {
211            DO => self.write(&[IAC, WONT, option]),
212            WILL => self.write(&[IAC, DONT, option]),
213            _ => Ok(()),
214        }
215    }
216}
217
218impl Terminal for Tn3270 {
219    fn size(&self) -> (usize, usize) {
220        (self.rows, self.columns)
221    }
222
223    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
224        let mut wire = Vec::with_capacity(stream.len() + 2);
225        for &b in stream {
226            wire.push(b);
227            if b == IAC {
228                wire.push(IAC);
229            }
230        }
231        wire.extend_from_slice(&[IAC, EOR_COMMAND]);
232        self.write(&wire)
233    }
234
235    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
236        if let Some(record) = self.pending.pop_front() {
237            return Ok(Some(record));
238        }
239        let mut record = Vec::new();
240        loop {
241            match self.item()? {
242                None if record.is_empty() => return Ok(None),
243                None => return Err("the terminal closed the connection inside a record".into()),
244                Some(Item::Data(b)) => record.push(b),
245                Some(Item::Eor) => return Ok(Some(record)),
246                Some(Item::Verb(verb, option)) if !matches!(option, EOR | BINARY) => self.refuse(verb, option)?,
247                Some(_) => {}
248            }
249        }
250    }
251}
252
253/// A terminal shared between the serving loop and the task that is using it.
254#[derive(Clone, Debug)]
255pub struct Shared(pub Rc<RefCell<Tn3270>>);
256
257impl Terminal for Shared {
258    fn size(&self) -> (usize, usize) {
259        self.0.borrow().size()
260    }
261
262    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
263        self.0.borrow_mut().send(stream)
264    }
265
266    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
267        self.0.borrow_mut().receive()
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use std::net::TcpListener;
275    use std::thread::JoinHandle;
276
277    fn read_exact(stream: &mut TcpStream, n: usize) -> Vec<u8> {
278        let mut bytes = vec![0; n];
279        stream.read_exact(&mut bytes).unwrap();
280        bytes
281    }
282
283    /// The client's side of the negotiation, reporting `terminal_type`; None for WONT TERMINAL-TYPE.
284    fn client_negotiates(stream: &mut TcpStream, terminal_type: Option<&str>) {
285        assert_eq!(read_exact(stream, 3), [IAC, DO, TERMINAL_TYPE]);
286        let Some(name) = terminal_type else {
287            stream.write_all(&[IAC, WONT, TERMINAL_TYPE]).unwrap();
288            return;
289        };
290        stream.write_all(&[IAC, WILL, TERMINAL_TYPE]).unwrap();
291        assert_eq!(read_exact(stream, 6), [IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, SE]);
292        let mut reply = vec![IAC, SB, TERMINAL_TYPE, TT_IS];
293        reply.extend_from_slice(name.as_bytes());
294        reply.extend_from_slice(&[IAC, SE]);
295        stream.write_all(&reply).unwrap();
296        assert_eq!(read_exact(stream, 12), [IAC, DO, EOR, IAC, WILL, EOR, IAC, DO, BINARY, IAC, WILL, BINARY]);
297        stream.write_all(&[IAC, WILL, BINARY, IAC, DO, BINARY, IAC, WILL, EOR, IAC, DO, EOR]).unwrap();
298    }
299
300    /// A connected pair: the server side negotiated, and the client thread run by `client`.
301    fn pair(terminal_type: &str, client: impl FnOnce(TcpStream) + Send + 'static) -> (Tn3270, JoinHandle<()>) {
302        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
303        let address = listener.local_addr().unwrap();
304        let name = terminal_type.to_string();
305        let handle = std::thread::spawn(move || {
306            let mut stream = TcpStream::connect(address).unwrap();
307            stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
308            client_negotiates(&mut stream, Some(&name));
309            client(stream);
310        });
311        let (server, _) = listener.accept().unwrap();
312        (negotiate(server).unwrap(), handle)
313    }
314
315    #[test]
316    fn negotiation_learns_the_terminal_type_and_size() {
317        let (t, client) = pair("IBM-3278-4", |_| {});
318        assert_eq!(t.terminal_type, "IBM-3278-4");
319        assert_eq!(t.size(), (43, 80));
320        client.join().unwrap();
321    }
322
323    #[test]
324    fn screen_sizes_follow_the_model_number() {
325        assert_eq!(screen_size("IBM-3278-2"), (24, 80));
326        assert_eq!(screen_size("IBM-3278-3"), (32, 80));
327        assert_eq!(screen_size("IBM-3278-5-E"), (27, 132));
328        assert_eq!(screen_size("IBM-3279-4-E"), (43, 80));
329        assert_eq!(screen_size("ibm-3279-3"), (32, 80));
330        assert_eq!(screen_size("IBM-DYNAMIC"), (24, 80));
331        assert_eq!(screen_size("VT100"), (24, 80));
332    }
333
334    #[test]
335    fn a_client_that_refuses_terminal_type_is_not_a_3270() {
336        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
337        let address = listener.local_addr().unwrap();
338        let client = std::thread::spawn(move || {
339            let mut stream = TcpStream::connect(address).unwrap();
340            client_negotiates(&mut stream, None);
341        });
342        let (server, _) = listener.accept().unwrap();
343        let error = negotiate(server).unwrap_err();
344        assert!(error.contains("not a 3270 emulator"), "{error}");
345        client.join().unwrap();
346    }
347
348    #[test]
349    fn negotiation_refuses_options_the_client_offers_unasked() {
350        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
351        let address = listener.local_addr().unwrap();
352        let client = std::thread::spawn(move || {
353            let mut stream = TcpStream::connect(address).unwrap();
354            stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
355            stream.write_all(&[IAC, WILL, 31]).unwrap();
356            assert_eq!(read_exact(&mut stream, 3), [IAC, DO, TERMINAL_TYPE]);
357            assert_eq!(read_exact(&mut stream, 3), [IAC, DONT, 31]);
358            stream.write_all(&[IAC, DO, 1, IAC, WILL, TERMINAL_TYPE]).unwrap();
359            assert_eq!(read_exact(&mut stream, 3), [IAC, WONT, 1]);
360            assert_eq!(read_exact(&mut stream, 6), [IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, SE]);
361            stream.write_all(&[IAC, SB, TERMINAL_TYPE, TT_IS, b'I', b'B', b'M', b'-', b'3', b'2', b'7', b'8', b'-', b'2', IAC, SE]).unwrap();
362            assert_eq!(read_exact(&mut stream, 12).len(), 12);
363            stream.write_all(&[IAC, DO, EOR, IAC, WILL, EOR, IAC, DO, BINARY, IAC, WILL, BINARY]).unwrap();
364        });
365        let (server, _) = listener.accept().unwrap();
366        let t = negotiate(server).unwrap();
367        assert_eq!(t.size(), (24, 80));
368        client.join().unwrap();
369    }
370
371    #[test]
372    fn send_doubles_iac_and_ends_the_record() {
373        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
374            assert_eq!(read_exact(&mut stream, 7), [0xF5, 0xC2, IAC, IAC, 0x01, IAC, EOR_COMMAND]);
375        });
376        t.send(&[0xF5, 0xC2, 0xFF, 0x01]).unwrap();
377        client.join().unwrap();
378    }
379
380    #[test]
381    fn receive_undoes_doubled_iac_and_skips_telnet_commands() {
382        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
383            let mut wire = vec![IAC, DO, 99, IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, IAC, IAC, SE, 0x7D, IAC, IAC, 0x40, IAC, 241];
384            wire.extend_from_slice(&[0x41, IAC, EOR_COMMAND]);
385            stream.write_all(&wire).unwrap();
386            assert_eq!(read_exact(&mut stream, 3), [IAC, WONT, 99]);
387        });
388        assert_eq!(t.receive().unwrap(), Some(vec![0x7D, IAC, 0x40, 0x41]));
389        assert_eq!(t.receive().unwrap(), None);
390        client.join().unwrap();
391    }
392
393    #[test]
394    fn a_pushed_back_record_comes_before_the_connection() {
395        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
396            stream.write_all(&[0x7D, 0x01, IAC, EOR_COMMAND]).unwrap();
397        });
398        t.push_back(vec![0xF3]);
399        t.push_back(vec![0xF4]);
400        assert_eq!(t.receive().unwrap(), Some(vec![0xF3]));
401        assert_eq!(t.receive().unwrap(), Some(vec![0xF4]));
402        assert_eq!(t.receive().unwrap(), Some(vec![0x7D, 0x01]));
403        assert_eq!(t.receive().unwrap(), None);
404        client.join().unwrap();
405    }
406}