1use 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
28const NEGOTIATION_TIMEOUT: Duration = Duration::from_secs(30);
30
31enum Item {
33 Data(u8),
34 Verb(u8, u8),
35 Sub(Vec<u8>),
36 Eor,
37 Other,
38}
39
40#[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#[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
68pub 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
80pub 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 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 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 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 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 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#[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 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 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}