Skip to main content

ironwork_rt/
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::terminal::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    /// Drops queued input no task read: a task's terminal input ends with the task.
127    pub fn discard_pending(&mut self) {
128        self.pending.clear();
129    }
130
131    fn write(&mut self, bytes: &[u8]) -> Result<(), String> {
132        self.stream.write_all(bytes).and_then(|()| self.stream.flush()).map_err(|e| format!("writing to the terminal: {e}"))
133    }
134
135    /// The next byte of the connection, None at end of stream.
136    fn byte(&mut self) -> Result<Option<u8>, String> {
137        if self.at == self.buffer.len() {
138            self.buffer.resize(4096, 0);
139            self.at = 0;
140            let n = loop {
141                match self.stream.read(&mut self.buffer) {
142                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
143                    Err(e) => {
144                        self.buffer.clear();
145                        return Err(format!("reading from the terminal: {e}"));
146                    }
147                    Ok(n) => break n,
148                }
149            };
150            self.buffer.truncate(n);
151            if n == 0 {
152                return Ok(None);
153            }
154        }
155        self.at += 1;
156        Ok(Some(self.buffer[self.at - 1]))
157    }
158
159    fn need(&mut self) -> Result<u8, String> {
160        self.byte()?.ok_or_else(|| "the terminal closed the connection inside a telnet command".to_string())
161    }
162
163    /// The next data byte or telnet command; None at end of stream between items.
164    fn item(&mut self) -> Result<Option<Item>, String> {
165        let Some(b) = self.byte()? else { return Ok(None) };
166        if b != IAC {
167            return Ok(Some(Item::Data(b)));
168        }
169        Ok(Some(match self.need()? {
170            IAC => Item::Data(IAC),
171            EOR_COMMAND => Item::Eor,
172            verb @ (DO | DONT | WILL | WONT) => Item::Verb(verb, self.need()?),
173            SB => {
174                let mut payload = Vec::new();
175                loop {
176                    match self.need()? {
177                        IAC => match self.need()? {
178                            SE => break,
179                            IAC => payload.push(IAC),
180                            _ => {}
181                        },
182                        b => payload.push(b),
183                    }
184                }
185                Item::Sub(payload)
186            }
187            _ => Item::Other,
188        }))
189    }
190
191    fn expect_item(&mut self) -> Result<Item, String> {
192        self.item()?.ok_or_else(|| "the terminal closed the connection during negotiation".to_string())
193    }
194
195    /// Records the client's word on an option, and refuses any option we do not use.
196    fn note(&mut self, verb: u8, option: u8, agreed: &mut Agreed) -> Result<(), String> {
197        let flag = match (option, verb) {
198            (TERMINAL_TYPE, WILL) => Some(&mut agreed.terminal_type),
199            (EOR, WILL) => Some(&mut agreed.eor_in),
200            (EOR, DO) => Some(&mut agreed.eor_out),
201            (BINARY, WILL) => Some(&mut agreed.binary_in),
202            (BINARY, DO) => Some(&mut agreed.binary_out),
203            (EOR | BINARY, WONT | DONT) => return Err(format!("the client refused telnet option {option}: this is not a 3270 emulator")),
204            _ => None,
205        };
206        match flag {
207            Some(flag) => *flag = true,
208            None => self.refuse(verb, option)?,
209        }
210        Ok(())
211    }
212
213    /// Answers a request for an option we do not use.
214    fn refuse(&mut self, verb: u8, option: u8) -> Result<(), String> {
215        match verb {
216            DO => self.write(&[IAC, WONT, option]),
217            WILL => self.write(&[IAC, DONT, option]),
218            _ => Ok(()),
219        }
220    }
221}
222
223impl Terminal for Tn3270 {
224    fn size(&self) -> (usize, usize) {
225        (self.rows, self.columns)
226    }
227
228    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
229        let mut wire = Vec::with_capacity(stream.len() + 2);
230        for &b in stream {
231            wire.push(b);
232            if b == IAC {
233                wire.push(IAC);
234            }
235        }
236        wire.extend_from_slice(&[IAC, EOR_COMMAND]);
237        self.write(&wire)
238    }
239
240    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
241        if let Some(record) = self.pending.pop_front() {
242            return Ok(Some(record));
243        }
244        let mut record = Vec::new();
245        loop {
246            match self.item()? {
247                None if record.is_empty() => return Ok(None),
248                None => return Err("the terminal closed the connection inside a record".into()),
249                Some(Item::Data(b)) => record.push(b),
250                Some(Item::Eor) => return Ok(Some(record)),
251                Some(Item::Verb(verb, option)) if !matches!(option, EOR | BINARY) => self.refuse(verb, option)?,
252                Some(_) => {}
253            }
254        }
255    }
256}
257
258/// A terminal shared between the serving loop and the task that is using it.
259#[derive(Clone, Debug)]
260pub struct Shared(pub Rc<RefCell<Tn3270>>);
261
262impl Terminal for Shared {
263    fn size(&self) -> (usize, usize) {
264        self.0.borrow().size()
265    }
266
267    fn send(&mut self, stream: &[u8]) -> Result<(), String> {
268        self.0.borrow_mut().send(stream)
269    }
270
271    fn receive(&mut self) -> Result<Option<Vec<u8>>, String> {
272        self.0.borrow_mut().receive()
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use std::net::TcpListener;
280    use std::thread::JoinHandle;
281
282    fn read_exact(stream: &mut TcpStream, n: usize) -> Vec<u8> {
283        let mut bytes = vec![0; n];
284        stream.read_exact(&mut bytes).unwrap();
285        bytes
286    }
287
288    /// The client's side of the negotiation, reporting `terminal_type`; None for WONT TERMINAL-TYPE.
289    fn client_negotiates(stream: &mut TcpStream, terminal_type: Option<&str>) {
290        assert_eq!(read_exact(stream, 3), [IAC, DO, TERMINAL_TYPE]);
291        let Some(name) = terminal_type else {
292            stream.write_all(&[IAC, WONT, TERMINAL_TYPE]).unwrap();
293            return;
294        };
295        stream.write_all(&[IAC, WILL, TERMINAL_TYPE]).unwrap();
296        assert_eq!(read_exact(stream, 6), [IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, SE]);
297        let mut reply = vec![IAC, SB, TERMINAL_TYPE, TT_IS];
298        reply.extend_from_slice(name.as_bytes());
299        reply.extend_from_slice(&[IAC, SE]);
300        stream.write_all(&reply).unwrap();
301        assert_eq!(read_exact(stream, 12), [IAC, DO, EOR, IAC, WILL, EOR, IAC, DO, BINARY, IAC, WILL, BINARY]);
302        stream.write_all(&[IAC, WILL, BINARY, IAC, DO, BINARY, IAC, WILL, EOR, IAC, DO, EOR]).unwrap();
303    }
304
305    /// A connected pair: the server side negotiated, and the client thread run by `client`.
306    fn pair(terminal_type: &str, client: impl FnOnce(TcpStream) + Send + 'static) -> (Tn3270, JoinHandle<()>) {
307        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
308        let address = listener.local_addr().unwrap();
309        let name = terminal_type.to_string();
310        let handle = std::thread::spawn(move || {
311            let mut stream = TcpStream::connect(address).unwrap();
312            stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
313            client_negotiates(&mut stream, Some(&name));
314            client(stream);
315        });
316        let (server, _) = listener.accept().unwrap();
317        (negotiate(server).unwrap(), handle)
318    }
319
320    #[test]
321    fn negotiation_learns_the_terminal_type_and_size() {
322        let (t, client) = pair("IBM-3278-4", |_| {});
323        assert_eq!(t.terminal_type, "IBM-3278-4");
324        assert_eq!(t.size(), (43, 80));
325        client.join().unwrap();
326    }
327
328    #[test]
329    fn screen_sizes_follow_the_model_number() {
330        assert_eq!(screen_size("IBM-3278-2"), (24, 80));
331        assert_eq!(screen_size("IBM-3278-3"), (32, 80));
332        assert_eq!(screen_size("IBM-3278-5-E"), (27, 132));
333        assert_eq!(screen_size("IBM-3279-4-E"), (43, 80));
334        assert_eq!(screen_size("ibm-3279-3"), (32, 80));
335        assert_eq!(screen_size("IBM-DYNAMIC"), (24, 80));
336        assert_eq!(screen_size("VT100"), (24, 80));
337    }
338
339    #[test]
340    fn a_client_that_refuses_terminal_type_is_not_a_3270() {
341        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
342        let address = listener.local_addr().unwrap();
343        let client = std::thread::spawn(move || {
344            let mut stream = TcpStream::connect(address).unwrap();
345            client_negotiates(&mut stream, None);
346        });
347        let (server, _) = listener.accept().unwrap();
348        let error = negotiate(server).unwrap_err();
349        assert!(error.contains("not a 3270 emulator"), "{error}");
350        client.join().unwrap();
351    }
352
353    #[test]
354    fn negotiation_refuses_options_the_client_offers_unasked() {
355        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
356        let address = listener.local_addr().unwrap();
357        let client = std::thread::spawn(move || {
358            let mut stream = TcpStream::connect(address).unwrap();
359            stream.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
360            stream.write_all(&[IAC, WILL, 31]).unwrap();
361            assert_eq!(read_exact(&mut stream, 3), [IAC, DO, TERMINAL_TYPE]);
362            assert_eq!(read_exact(&mut stream, 3), [IAC, DONT, 31]);
363            stream.write_all(&[IAC, DO, 1, IAC, WILL, TERMINAL_TYPE]).unwrap();
364            assert_eq!(read_exact(&mut stream, 3), [IAC, WONT, 1]);
365            assert_eq!(read_exact(&mut stream, 6), [IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, SE]);
366            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();
367            assert_eq!(read_exact(&mut stream, 12).len(), 12);
368            stream.write_all(&[IAC, DO, EOR, IAC, WILL, EOR, IAC, DO, BINARY, IAC, WILL, BINARY]).unwrap();
369        });
370        let (server, _) = listener.accept().unwrap();
371        let t = negotiate(server).unwrap();
372        assert_eq!(t.size(), (24, 80));
373        client.join().unwrap();
374    }
375
376    #[test]
377    fn send_doubles_iac_and_ends_the_record() {
378        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
379            assert_eq!(read_exact(&mut stream, 7), [0xF5, 0xC2, IAC, IAC, 0x01, IAC, EOR_COMMAND]);
380        });
381        t.send(&[0xF5, 0xC2, 0xFF, 0x01]).unwrap();
382        client.join().unwrap();
383    }
384
385    #[test]
386    fn receive_undoes_doubled_iac_and_skips_telnet_commands() {
387        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
388            let mut wire = vec![IAC, DO, 99, IAC, SB, TERMINAL_TYPE, TT_SEND, IAC, IAC, IAC, SE, 0x7D, IAC, IAC, 0x40, IAC, 241];
389            wire.extend_from_slice(&[0x41, IAC, EOR_COMMAND]);
390            stream.write_all(&wire).unwrap();
391            assert_eq!(read_exact(&mut stream, 3), [IAC, WONT, 99]);
392        });
393        assert_eq!(t.receive().unwrap(), Some(vec![0x7D, IAC, 0x40, 0x41]));
394        assert_eq!(t.receive().unwrap(), None);
395        client.join().unwrap();
396    }
397
398    #[test]
399    fn a_pushed_back_record_comes_before_the_connection() {
400        let (mut t, client) = pair("IBM-3278-2", |mut stream| {
401            stream.write_all(&[0x7D, 0x01, IAC, EOR_COMMAND]).unwrap();
402        });
403        t.push_back(vec![0xF3]);
404        t.push_back(vec![0xF4]);
405        assert_eq!(t.receive().unwrap(), Some(vec![0xF3]));
406        assert_eq!(t.receive().unwrap(), Some(vec![0xF4]));
407        assert_eq!(t.receive().unwrap(), Some(vec![0x7D, 0x01]));
408        assert_eq!(t.receive().unwrap(), None);
409        client.join().unwrap();
410    }
411}