1use 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
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 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 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 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 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 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#[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 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 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}