├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── run_unit_tests.sh ├── setup.py ├── tests ├── context.py ├── test_attributes.py ├── test_datastream.py ├── test_emulator.py └── test_telnet.py └── tn3270 ├── __about__.py ├── __init__.py ├── attributes.py ├── datastream.py ├── ebcdic.py ├── emulator.py ├── structured_fields.py └── telnet.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up Python 3.8 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: 3.8 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install pylint build 22 | pip install -r requirements.txt 23 | 24 | - name: Run linter 25 | run: pylint -E tn3270 26 | 27 | - name: Run unit tests 28 | run: ./run_unit_tests.sh 29 | 30 | - name: Build packages 31 | run: python -m build 32 | 33 | - name: Attach packages 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: dist 37 | path: dist/ 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | VIRTUALENV/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Andrew Kay 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytn3270 2 | 3 | Python TN3270 library. 4 | 5 | Inspired by [pyte](https://github.com/selectel/pyte), this is a pure Python TN3270 library providing data stream parsing and in-memory emulation. It does not include a user interface or routines to support automation, instead it is designed to be used to build user-facing emulators and automation libraries. 6 | 7 | ## Features 8 | 9 | pytn3270 is a work in progress and only supports basic TN3270 emulation. 10 | 11 | ## Usage 12 | 13 | Install using `pip`: 14 | 15 | ``` 16 | pip install pytn3270 17 | ``` 18 | 19 | Connect to a mainframe: 20 | 21 | ``` 22 | from tn3270 import Telnet, Emulator, AID, CharacterCell 23 | 24 | telnet = Telnet('IBM-3279-2-E') 25 | 26 | telnet.open('mainframe', 23) 27 | 28 | emulator = Emulator(telnet, 24, 80) 29 | 30 | # Wait until the keyboard is unlocked. 31 | while emulator.keyboard_locked: 32 | print('Waiting for keyboard to be unlocked...') 33 | 34 | emulator.update(timeout=1) 35 | 36 | # Convert the screen contents to a string, replacing attribute cells with '@'. 37 | # 38 | # Note that this is not supposed to demonstrate an efficient implementation. 39 | screen = '' 40 | 41 | for cell in emulator.cells: 42 | if isinstance(cell, CharacterCell): 43 | byte = cell.byte 44 | 45 | if byte == 0: 46 | screen += ' ' 47 | else: 48 | screen += bytes([byte]).decode('ibm037') 49 | else: 50 | screen += '@' 51 | 52 | # Display the screen. 53 | for line in [screen[index:index+80] for index in range(0, len(screen), 80)]: 54 | print(line) 55 | ``` 56 | 57 | ## References 58 | 59 | If you are looking for information on the TN3270 protocol I'd recommend the 60 | following resources: 61 | 62 | * Steve Millington's [TN3270 Protocol Cheat Sheet](http://ruelgnoj.co.uk/3270/) 63 | 64 | For information on the 3270 data stream (as used by TN3270) I'd recommend: 65 | 66 | * IBM [3270 Data Stream Programmer's Reference](https://bitsavers.computerhistory.org/pdf/ibm/3270/GA23-0059-4_3270_Data_Stream_Programmers_Reference_Dec88.pdf) (GA23-0059-4) 67 | * IBM CICS [The 3270 Family of Terminals](https://www.ibm.com/support/knowledgecenter/en/SSGMGV_3.1.0/com.ibm.cics.ts31.doc/dfhp3/dfhp3bg.htm#DFHP3BG) 68 | * Greg Price's [3270 Programming Overview](http://www.prycroft6.com.au/misc/3270.html) 69 | * Tommy Sprinkles' [3270 Data Stream Programming](https://www.tommysprinkle.com/mvs/P3270/start.htm) 70 | 71 | ## See Also 72 | 73 | * [oec](https://github.com/lowobservable/oec) - IBM 3270 terminal controller 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | telnetlib3==2.0.4 2 | -------------------------------------------------------------------------------- /run_unit_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Display verbose output in CI environment. 4 | if [ -n "$CI" ]; then 5 | OPTS=-v 6 | fi 7 | 8 | python -m unittest discover $OPTS tests 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | ABOUT = {} 5 | 6 | with open(os.path.join(os.path.dirname(__file__), "tn3270", "__about__.py")) as file: 7 | exec(file.read(), ABOUT) 8 | 9 | LONG_DESCRIPTION = """# pytn3270 10 | 11 | Python TN3270 library. 12 | 13 | See [GitHub](https://github.com/lowobservable/pytn3270#readme) for more information. 14 | """ 15 | 16 | setup( 17 | name='pytn3270', 18 | version=ABOUT['__version__'], 19 | description='TN3270 library', 20 | url='https://github.com/lowobservable/pytn3270', 21 | author='Andrew Kay', 22 | author_email='projects@ajk.me', 23 | packages=['tn3270'], 24 | install_requires=['telnetlib3'], 25 | long_description=LONG_DESCRIPTION, 26 | long_description_content_type='text/markdown', 27 | classifiers=[ 28 | 'Development Status :: 3 - Alpha', 29 | 'License :: OSI Approved :: ISC License (ISCL)', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python :: 3', 32 | 'Topic :: Communications', 33 | 'Topic :: Terminals' 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 5 | -------------------------------------------------------------------------------- /tests/test_attributes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import context 4 | 5 | from tn3270.attributes import Attribute 6 | 7 | class AttributeTestCase(unittest.TestCase): 8 | def test_protected(self): 9 | # Act 10 | attribute = Attribute(0b00100000) 11 | 12 | # Assert 13 | self.assertTrue(attribute.protected) 14 | self.assertFalse(attribute.numeric) 15 | self.assertFalse(attribute.skip) 16 | self.assertFalse(attribute.intensified) 17 | self.assertFalse(attribute.hidden) 18 | self.assertFalse(attribute.modified) 19 | 20 | def test_numeric(self): 21 | # Act 22 | attribute = Attribute(0b00010000) 23 | 24 | # Assert 25 | self.assertFalse(attribute.protected) 26 | self.assertTrue(attribute.numeric) 27 | self.assertFalse(attribute.skip) 28 | self.assertFalse(attribute.intensified) 29 | self.assertFalse(attribute.hidden) 30 | self.assertFalse(attribute.modified) 31 | 32 | def test_skip(self): 33 | # Act 34 | attribute = Attribute(0b00110000) 35 | 36 | # Assert 37 | self.assertTrue(attribute.protected) 38 | self.assertTrue(attribute.numeric) 39 | self.assertTrue(attribute.skip) 40 | self.assertFalse(attribute.intensified) 41 | self.assertFalse(attribute.hidden) 42 | self.assertFalse(attribute.modified) 43 | 44 | def test_intensified(self): 45 | # Act 46 | attribute = Attribute(0b00001000) 47 | 48 | # Assert 49 | self.assertFalse(attribute.protected) 50 | self.assertFalse(attribute.numeric) 51 | self.assertFalse(attribute.skip) 52 | self.assertTrue(attribute.intensified) 53 | self.assertFalse(attribute.hidden) 54 | self.assertFalse(attribute.modified) 55 | 56 | def test_hidden(self): 57 | # Act 58 | attribute = Attribute(0b00001100) 59 | 60 | # Assert 61 | self.assertFalse(attribute.protected) 62 | self.assertFalse(attribute.numeric) 63 | self.assertFalse(attribute.skip) 64 | self.assertFalse(attribute.intensified) 65 | self.assertTrue(attribute.hidden) 66 | self.assertFalse(attribute.modified) 67 | 68 | def test_modified(self): 69 | # Act 70 | attribute = Attribute(0b00000001) 71 | 72 | # Assert 73 | self.assertFalse(attribute.protected) 74 | self.assertFalse(attribute.numeric) 75 | self.assertFalse(attribute.skip) 76 | self.assertFalse(attribute.intensified) 77 | self.assertFalse(attribute.hidden) 78 | self.assertTrue(attribute.modified) 79 | -------------------------------------------------------------------------------- /tests/test_datastream.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import context 4 | 5 | from tn3270.attributes import Attribute, ExtendedAttribute, HighlightExtendedAttribute, ForegroundColorExtendedAttribute 6 | from tn3270.datastream import Command, Order, AID, WCC, parse_outbound_message, format_inbound_read_modified_message, parse_orders, parse_outbound_structured_fields, format_inbound_structured_fields, parse_extended_attribute, parse_address, format_address 7 | 8 | class WCCTestCase(unittest.TestCase): 9 | def test_reset(self): 10 | # Act 11 | wcc = WCC(0b01000000) 12 | 13 | # Assert 14 | self.assertTrue(wcc.reset) 15 | self.assertFalse(wcc.alarm) 16 | self.assertFalse(wcc.unlock_keyboard) 17 | self.assertFalse(wcc.reset_modified) 18 | 19 | def test_alarm(self): 20 | # Act 21 | wcc = WCC(0b00000100) 22 | 23 | # Assert 24 | self.assertFalse(wcc.reset) 25 | self.assertTrue(wcc.alarm) 26 | self.assertFalse(wcc.unlock_keyboard) 27 | self.assertFalse(wcc.reset_modified) 28 | 29 | def test_unlock_keyboard(self): 30 | # Act 31 | wcc = WCC(0b00000010) 32 | 33 | # Assert 34 | self.assertFalse(wcc.reset) 35 | self.assertFalse(wcc.alarm) 36 | self.assertTrue(wcc.unlock_keyboard) 37 | self.assertFalse(wcc.reset_modified) 38 | 39 | def test_reset_modified(self): 40 | # Act 41 | wcc = WCC(0b00000001) 42 | 43 | # Assert 44 | self.assertFalse(wcc.reset) 45 | self.assertFalse(wcc.alarm) 46 | self.assertFalse(wcc.unlock_keyboard) 47 | self.assertTrue(wcc.reset_modified) 48 | 49 | class ParseOutboundMessageTestCase(unittest.TestCase): 50 | def test_write(self): 51 | # Act 52 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 53 | 54 | # Assert 55 | self.assertEqual(command, Command.W) 56 | 57 | self.assertIsInstance(wcc, WCC) 58 | self.assertEqual(wcc.value, 0xc3) 59 | 60 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 61 | 62 | def test_sna_write(self): 63 | # Act 64 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('f1 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 65 | 66 | # Assert 67 | self.assertEqual(command, Command.W) 68 | 69 | self.assertIsInstance(wcc, WCC) 70 | self.assertEqual(wcc.value, 0xc3) 71 | 72 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 73 | 74 | def test_read_buffer(self): 75 | self.assertEqual(parse_outbound_message(bytes.fromhex('02')), (Command.RB,)) 76 | 77 | def test_sna_read_buffer(self): 78 | self.assertEqual(parse_outbound_message(bytes.fromhex('f2')), (Command.RB,)) 79 | 80 | def test_nop(self): 81 | self.assertEqual(parse_outbound_message(bytes.fromhex('03')), (Command.NOP,)) 82 | 83 | def test_erase_write(self): 84 | # Act 85 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('05 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 86 | 87 | # Assert 88 | self.assertEqual(command, Command.EW) 89 | 90 | self.assertIsInstance(wcc, WCC) 91 | self.assertEqual(wcc.value, 0xc3) 92 | 93 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 94 | 95 | def test_sna_erase_write(self): 96 | # Act 97 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('f5 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 98 | 99 | # Assert 100 | self.assertEqual(command, Command.EW) 101 | 102 | self.assertIsInstance(wcc, WCC) 103 | self.assertEqual(wcc.value, 0xc3) 104 | 105 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 106 | 107 | def test_read_modified(self): 108 | self.assertEqual(parse_outbound_message(bytes.fromhex('06')), (Command.RM,)) 109 | 110 | def test_sna_read_modified(self): 111 | self.assertEqual(parse_outbound_message(bytes.fromhex('f6')), (Command.RM,)) 112 | 113 | def test_erase_write_alternate(self): 114 | # Act 115 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('0d c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 116 | 117 | # Assert 118 | self.assertEqual(command, Command.EWA) 119 | 120 | self.assertIsInstance(wcc, WCC) 121 | self.assertEqual(wcc.value, 0xc3) 122 | 123 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 124 | 125 | def test_sna_erase_write_alternate(self): 126 | # Act 127 | (command, wcc, orders) = parse_outbound_message(bytes.fromhex('7e c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 128 | 129 | # Assert 130 | self.assertEqual(command, Command.EWA) 131 | 132 | self.assertIsInstance(wcc, WCC) 133 | self.assertEqual(wcc.value, 0xc3) 134 | 135 | self.assertEqual([order[0] for order in orders], [Order.SBA, Order.SF, None]) 136 | 137 | def test_read_modified_all(self): 138 | self.assertEqual(parse_outbound_message(bytes.fromhex('0e')), (Command.RMA,)) 139 | 140 | def test_sna_read_modified_all(self): 141 | self.assertEqual(parse_outbound_message(bytes.fromhex('63')), (Command.RMA,)) 142 | 143 | def test_erase_all_unprotected(self): 144 | self.assertEqual(parse_outbound_message(bytes.fromhex('0f')), (Command.EAU,)) 145 | 146 | def test_sna_all_unprotected(self): 147 | self.assertEqual(parse_outbound_message(bytes.fromhex('6f')), (Command.EAU,)) 148 | 149 | def test_write_structured_field(self): 150 | # Act 151 | (command, structured_fields) = parse_outbound_message(bytes.fromhex('11 00 07 01 01 02 03 04 00 00 02 05 06 07')) 152 | 153 | # Assert 154 | self.assertEqual(command, Command.WSF) 155 | 156 | self.assertEqual([field[0] for field in structured_fields], [1, 2]) 157 | 158 | def test_sna_write_structured_field(self): 159 | # Act 160 | (command, structured_fields) = parse_outbound_message(bytes.fromhex('f3 00 07 01 01 02 03 04 00 00 02 05 06 07')) 161 | 162 | # Assert 163 | self.assertEqual(command, Command.WSF) 164 | 165 | self.assertEqual([field[0] for field in structured_fields], [1, 2]) 166 | 167 | def test_unrecognized_command(self): 168 | with self.assertRaisesRegex(ValueError, 'Unrecognized command 0x99'): 169 | parse_outbound_message(bytes.fromhex('99')) 170 | 171 | class FormatInboundReadModifiedMessageTestCase(unittest.TestCase): 172 | def test_enter(self): 173 | # Act 174 | bytes_ = format_inbound_read_modified_message(AID.ENTER, 800, [(Order.SBA, [10]), (None, bytes.fromhex('00 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4 00'))]) 175 | 176 | # Assert 177 | self.assertEqual(bytes_, bytes.fromhex('7d 4c 60 11 40 4a 00 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4 00')) 178 | 179 | def test_clear(self): 180 | # Act 181 | bytes_ = format_inbound_read_modified_message(AID.CLEAR, 800, [(Order.SBA, [10]), (None, bytes.fromhex('00 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4 00'))]) 182 | 183 | # Assert 184 | self.assertEqual(bytes_, bytes.fromhex('6d')) 185 | 186 | def test_clear_with_all(self): 187 | # Act 188 | bytes_ = format_inbound_read_modified_message(AID.CLEAR, 800, [(Order.SBA, [10]), (None, bytes.fromhex('00 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4 00'))], all_=True) 189 | 190 | # Assert 191 | self.assertEqual(bytes_, bytes.fromhex('6d 4c 60 11 40 4a 00 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4 00')) 192 | 193 | # TODO: Separate strip nulls test? 194 | 195 | # TODO: Multiple fields 196 | 197 | class ParseOrdersTestCase(unittest.TestCase): 198 | def test(self): 199 | # Act 200 | orders = list(parse_orders(bytes.fromhex('11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'))) 201 | 202 | # Assert 203 | self.assertEqual(orders[0], (Order.SBA, [752])) 204 | 205 | self.assertEqual(orders[1][0], Order.SF) 206 | 207 | self.assertIsInstance(orders[1][1][0], Attribute) 208 | self.assertEqual(orders[1][1][0].value, 0xf8) 209 | 210 | self.assertEqual(orders[2], (None, bytes.fromhex('c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'))) 211 | 212 | def test_program_tab(self): 213 | self.assertEqual(list(parse_orders(bytes.fromhex('05'))), [(Order.PT, None)]) 214 | 215 | def test_graphic_escape(self): 216 | self.assertEqual(list(parse_orders(bytes.fromhex('08 ad'))), [(Order.GE, [0xad])]) 217 | 218 | def test_set_buffer_address(self): 219 | self.assertEqual(list(parse_orders(bytes.fromhex('11 4b f0'))), [(Order.SBA, [752])]) 220 | 221 | def test_erase_unprotected_to_address(self): 222 | self.assertEqual(list(parse_orders(bytes.fromhex('12 4b f0'))), [(Order.EUA, [752])]) 223 | 224 | def test_insert_cursor(self): 225 | self.assertEqual(list(parse_orders(bytes.fromhex('13'))), [(Order.IC, None)]) 226 | 227 | def test_start_field(self): 228 | # Act 229 | orders = list(parse_orders(bytes.fromhex('1d f8'))) 230 | 231 | # Assert 232 | self.assertEqual(orders[0][0], Order.SF) 233 | 234 | self.assertIsInstance(orders[0][1][0], Attribute) 235 | self.assertEqual(orders[0][1][0].value, 0xf8) 236 | 237 | def test_set_attribute(self): 238 | # Act 239 | orders = list(parse_orders(bytes.fromhex('28 41 f2'))) 240 | 241 | # Assert 242 | self.assertEqual(orders[0][0], Order.SA) 243 | 244 | self.assertIsInstance(orders[0][1][0], HighlightExtendedAttribute) 245 | self.assertTrue(orders[0][1][0].reverse) 246 | 247 | def test_start_field_extended(self): 248 | # Act 249 | orders = list(parse_orders(bytes.fromhex('29 02 c0 60 42 f1'))) 250 | 251 | # Assert 252 | self.assertEqual(orders[0][0], Order.SFE) 253 | 254 | self.assertIsInstance(orders[0][1][0], Attribute) 255 | self.assertEqual(orders[0][1][0].value, 0x60) 256 | 257 | extended_attributes = orders[0][1][1] 258 | 259 | self.assertEqual(len(extended_attributes), 1) 260 | 261 | self.assertIsInstance(extended_attributes[0], ForegroundColorExtendedAttribute) 262 | self.assertEqual(extended_attributes[0].color, 0xf1) 263 | 264 | def test_modify_field(self): 265 | # Act 266 | orders = list(parse_orders(bytes.fromhex('2c 02 c0 60 42 f1'))) 267 | 268 | # Assert 269 | self.assertEqual(orders[0][0], Order.MF) 270 | 271 | self.assertIsInstance(orders[0][1][0], Attribute) 272 | self.assertEqual(orders[0][1][0].value, 0x60) 273 | 274 | extended_attributes = orders[0][1][1] 275 | 276 | self.assertEqual(len(extended_attributes), 1) 277 | 278 | self.assertIsInstance(extended_attributes[0], ForegroundColorExtendedAttribute) 279 | self.assertEqual(extended_attributes[0].color, 0xf1) 280 | 281 | def test_repeat_to_address(self): 282 | self.assertEqual(list(parse_orders(bytes.fromhex('3c 4b f0 c1'))), [(Order.RA, [752, 0xc1, False])]) 283 | 284 | def test_repeat_to_address_graphic_escape(self): 285 | self.assertEqual(list(parse_orders(bytes.fromhex('3c 4b f0 08 ad'))), [(Order.RA, [752, 0xad, True])]) 286 | 287 | class ParseOutboundStructuredFieldsTestCase(unittest.TestCase): 288 | def test(self): 289 | self.assertEqual(list(parse_outbound_structured_fields(bytes.fromhex('00 07 01 01 02 03 04 00 00 02 05 06 07'))), [(1, bytes.fromhex('01 02 03 04')), (2, bytes.fromhex('05 06 07'))]) 290 | 291 | def test_invalid_field(self): 292 | for bytes_ in [bytes.fromhex('01'), bytes.fromhex('01 02')]: 293 | with self.subTest(bytes_=bytes_): 294 | with self.assertRaises(Exception): 295 | list(parse_outbound_structured_fields(bytes_)) 296 | 297 | def test_invalid_length(self): 298 | for bytes_ in [bytes.fromhex('00 02 01 01'), bytes.fromhex('00 05 01 01')]: 299 | with self.subTest(bytes_=bytes_): 300 | with self.assertRaises(Exception): 301 | list(parse_outbound_structured_fields(bytes_)) 302 | 303 | class FormatInboundStructuredFieldsTestCase(unittest.TestCase): 304 | def test(self): 305 | # Act 306 | bytes_ = format_inbound_structured_fields([(0x81, bytes.fromhex('80 80'))]) 307 | 308 | # Assert 309 | self.assertEqual(bytes_, bytes.fromhex('88 00 05 81 80 80')) 310 | 311 | class ParseExtendedAttributeTestCase(unittest.TestCase): 312 | def test_highlight(self): 313 | extended_attribute = parse_extended_attribute(bytes.fromhex('41 f2')) 314 | 315 | self.assertIsInstance(extended_attribute, HighlightExtendedAttribute) 316 | 317 | def test_foreground_color(self): 318 | extended_attribute = parse_extended_attribute(bytes.fromhex('42 00')) 319 | 320 | self.assertIsInstance(extended_attribute, ForegroundColorExtendedAttribute) 321 | 322 | def test_unsupported(self): 323 | extended_attribute = parse_extended_attribute(bytes.fromhex('43 00')) 324 | 325 | self.assertIsInstance(extended_attribute, ExtendedAttribute) 326 | 327 | self.assertEqual(extended_attribute.type_, 0x43) 328 | self.assertEqual(extended_attribute.value, 0x00) 329 | 330 | def test_invalid(self): 331 | with self.assertRaises(Exception): 332 | parse_extended_attribute(bytes.fromhex('00')) 333 | 334 | class ParseAddressTestCase(unittest.TestCase): 335 | def test_12_bit_address_with_01_prefix(self): 336 | (address, size) = parse_address(bytes([0b01000000, 0b01111100])) 337 | 338 | self.assertEqual(address, 60) 339 | self.assertEqual(size, 12) 340 | 341 | def test_12_bit_address_with_11_prefix(self): 342 | (address, size) = parse_address(bytes([0b11000010, 0b01100000])) 343 | 344 | self.assertEqual(address, 160) 345 | self.assertEqual(size, 12) 346 | 347 | def test_14_bit_address(self): 348 | (address, size) = parse_address(bytes([0b00000011, 0b00100000])) 349 | 350 | self.assertEqual(address, 800) 351 | self.assertEqual(size, 14) 352 | 353 | def test_16_bit_address(self): 354 | (address, size) = parse_address(bytes([0b00001100, 0b00011100]), size=16) 355 | 356 | self.assertEqual(address, 3100) 357 | self.assertEqual(size, 16) 358 | 359 | class FormatAddressTestCase(unittest.TestCase): 360 | def test_12_bit_address(self): 361 | self.assertEqual(format_address(160, size=12), bytes([0b11000010, 0b01100000])) 362 | 363 | def test_14_bit_address(self): 364 | self.assertEqual(format_address(800, size=14), bytes([0b00000011, 0b00100000])) 365 | 366 | def test_16_bit_address(self): 367 | self.assertEqual(format_address(3100, size=16), bytes([0b00001100, 0b00011100])) 368 | 369 | def test_invalid_size(self): 370 | with self.assertRaisesRegex(ValueError, 'Invalid size'): 371 | format_address(3100, size=13) 372 | -------------------------------------------------------------------------------- /tests/test_emulator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, create_autospec 3 | 4 | import string 5 | 6 | import context 7 | 8 | from tn3270.telnet import Telnet, TN3270EMessageHeader, TN3270EDataType, TN3270EResponseFlag 9 | from tn3270.emulator import Emulator, AttributeCell, CharacterCell, ProtectedCellOperatorError, FieldOverflowOperatorError 10 | from tn3270.datastream import AID 11 | 12 | SCREEN1 = bytes.fromhex(('05c3110000e2d6d4c5e3c8c9d5c740c9d540e3c8c540c6c9d9e2e340d9d6e611' 13 | '00503c00a07e110154d5d6d9d4c1d3110168c9d5e3c5d5e2c511017cc8c9c4c4' 14 | 'c5d5110190d7d9d6e3c5c3e3c5c41101a31d60e7e7e7e7e7e7e7e7e7e71101b7' 15 | '1de8e7e7e7e7e7e7e7e7e7e71101cb1d6ce7e7e7e7e7e7e7e7e7e71d601101e0' 16 | 'e4d5d7d9d6e3c5c3e3c5c41101f31d401101fe1df01102071dc81102121df011' 17 | '021b1d4c1102261df0110230d5e4d4c5d9c9c31102431d5011024e1df0110257' 18 | '1dd81102621df011026b1d5c1102761df0110280d7d9c5c6c9d3d3c5c4110293' 19 | '1d40e7e7e7e7e711029e1df01102a71dc8e7e7e7e7e71102b21df01102bb1d4c' 20 | 'e7e7e7e7e71102c61df01102d0d4d6c4c9c6c9c5c41102e31dc1e7e7e7e7e711' 21 | '02ee1df01102f71dc9e7e7e7e7e71103021df011030b1d4de7e7e7e7e7110316' 22 | '1df0110370d5d640e2d2c9d71103831d4011038e1d601101f813')) 23 | 24 | SCREEN2 = bytes.fromhex('05c11100151d304c606011076760606e1d00') 25 | 26 | SCREEN3 = bytes.fromhex(('05c311000060606e1d001101a41d304c60601101b860606e1d001101c21d304c' 27 | '60601101f460606e1d001101fe1d304c606011020860606e1d001102121d304c' 28 | '606011000413')) 29 | 30 | SCREEN4 = bytes.fromhex('05c1110015e38889a240a283998585954089a240a4958696999481a3a38584') 31 | 32 | class UpdateTestCase(unittest.TestCase): 33 | def setUp(self): 34 | self.stream = create_autospec(Telnet, instance=True) 35 | 36 | self.emulator = Emulator(self.stream, 43, 80) 37 | 38 | def test_no_message(self): 39 | # Arrange 40 | self.stream.read_multiple = Mock(return_value=[]) 41 | 42 | # Act and assert 43 | self.assertFalse(self.emulator.update()) 44 | 45 | def test_write(self): 46 | # Arrange 47 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), None)]) 48 | 49 | # Act and assert 50 | self.assertTrue(self.emulator.update()) 51 | 52 | self.assertIsInstance(self.emulator.cells[752], AttributeCell) 53 | self.assertEqual(self.emulator.get_bytes(753, 763), bytes.fromhex('c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4')) 54 | 55 | def test_write_wrap(self): 56 | # Arrange 57 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 07 7c c1 c2 c3 c4 c5 c6 c7 c8'), None)]) 58 | 59 | # Act and assert 60 | self.assertTrue(self.emulator.update()) 61 | 62 | self.assertEqual(self.emulator.get_bytes(0, 3), bytes.fromhex('c5 c6 c7 c8')) 63 | self.assertEqual(self.emulator.get_bytes(1916, 1919), bytes.fromhex('c1 c2 c3 c4')) 64 | 65 | def test_write_alarm(self): 66 | # Arrange 67 | self.emulator.alarm = Mock() 68 | 69 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c7'), None)]) 70 | 71 | # Act 72 | self.emulator.update() 73 | 74 | # Assert 75 | self.emulator.alarm.assert_called() 76 | 77 | def test_read_buffer(self): 78 | # Arrange 79 | self.stream.read_multiple = Mock(side_effect=[[(SCREEN1, None)], [(bytes.fromhex('02'), None)]]) 80 | 81 | self.emulator.update() 82 | 83 | self.emulator.cursor_address = 505 84 | 85 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 86 | self.emulator.input(character) 87 | 88 | self.assertEqual(self.emulator.cursor_address, 525) 89 | 90 | self.emulator.aid(AID.ENTER) 91 | 92 | self.stream.write.reset_mock() 93 | 94 | # Act 95 | self.emulator.update() 96 | 97 | # Assert 98 | self.stream.write.assert_called() 99 | 100 | bytes_ = self.stream.write.mock_calls[0][1][0] 101 | 102 | self.assertEqual(bytes_[:3], bytes.fromhex('7dc84d')) 103 | self.assertEqual(bytes_[3:944], bytes.fromhex('e2d6d4c5e3c8c9d5c740c9d540e3c8c540c6c9d9e2e340d9d6e60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5d6d9d4c1d30000000000000000000000000000c9d5e3c5d5e2c500000000000000000000000000c8c9c4c4c5d50000000000000000000000000000d7d9d6e3c5c3e3c5c4000000000000000000001d60e7e7e7e7e7e7e7e7e7e70000000000000000001de8e7e7e7e7e7e7e7e7e7e70000000000000000001d6ce7e7e7e7e7e7e7e7e7e71d60000000000000000000e4d5d7d9d6e3c5c3e3c5c400000000000000001d410000000000c1c2c3c4c51df000000000000000001dc9c6c7c8c9d100000000001df000000000000000001d4c000000000000000000001df0000000000000000000d5e4d4c5d9c9c30000000000000000000000001d50000000000000000000001df000000000000000001dd8000000000000000000001df000000000000000001d5c000000000000000000001df0000000000000000000d7d9c5c6c9d3d3c5c4000000000000000000001d40e7e7e7e7e700000000001df000000000000000001dc8e7e7e7e7e700000000001df000000000000000001d4ce7e7e7e7e700000000001df0000000000000000000d4d6c4c9c6c9c5c400000000000000000000001dc1e7e7e7e7e700000000001df000000000000000001dc9e7e7e7e7e700000000001df000000000000000001d4de7e7e7e7e700000000001df00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d5d640e2d2c9d70000000000000000000000001d40000000000000000000001d60')) 104 | self.assertTrue(all([byte == 0x00 for byte in bytes_[944:]])) 105 | 106 | def test_nop(self): 107 | # Arrange 108 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('03'), None)]) 109 | 110 | # Act 111 | self.emulator.update() 112 | 113 | def test_erase_write_screen1(self): 114 | # Arrange 115 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 116 | 117 | # Act 118 | self.emulator.update() 119 | 120 | # Assert 121 | self.assertFalse(self.emulator.alternate) 122 | 123 | self.assertEqual(self.emulator.cursor_address, 504) 124 | 125 | fields = self.emulator.get_fields(protected=False) 126 | 127 | self.assertEqual(len(fields), 13) 128 | 129 | self.assertEqual(fields[0][0], 500) 130 | self.assertEqual(fields[0][1], 509) 131 | self.assertFalse(fields[0][2].protected) 132 | self.assertFalse(fields[0][2].numeric) 133 | self.assertFalse(fields[0][2].intensified) 134 | self.assertFalse(fields[0][2].hidden) 135 | self.assertFalse(fields[0][2].modified) 136 | 137 | self.assertEqual(fields[1][0], 520) 138 | self.assertEqual(fields[1][1], 529) 139 | self.assertFalse(fields[1][2].protected) 140 | self.assertFalse(fields[1][2].numeric) 141 | self.assertTrue(fields[1][2].intensified) 142 | self.assertFalse(fields[1][2].hidden) 143 | self.assertFalse(fields[1][2].modified) 144 | 145 | self.assertEqual(fields[2][0], 540) 146 | self.assertEqual(fields[2][1], 549) 147 | self.assertFalse(fields[2][2].protected) 148 | self.assertFalse(fields[2][2].numeric) 149 | self.assertFalse(fields[2][2].intensified) 150 | self.assertTrue(fields[2][2].hidden) 151 | self.assertFalse(fields[2][2].modified) 152 | 153 | self.assertEqual(fields[3][0], 580) 154 | self.assertEqual(fields[3][1], 589) 155 | self.assertFalse(fields[3][2].protected) 156 | self.assertTrue(fields[3][2].numeric) 157 | self.assertFalse(fields[3][2].intensified) 158 | self.assertFalse(fields[3][2].hidden) 159 | self.assertFalse(fields[3][2].modified) 160 | 161 | self.assertEqual(fields[4][0], 600) 162 | self.assertEqual(fields[4][1], 609) 163 | self.assertFalse(fields[4][2].protected) 164 | self.assertTrue(fields[4][2].numeric) 165 | self.assertTrue(fields[4][2].intensified) 166 | self.assertFalse(fields[4][2].hidden) 167 | self.assertFalse(fields[4][2].modified) 168 | 169 | self.assertEqual(fields[5][0], 620) 170 | self.assertEqual(fields[5][1], 629) 171 | self.assertFalse(fields[5][2].protected) 172 | self.assertTrue(fields[5][2].numeric) 173 | self.assertFalse(fields[5][2].intensified) 174 | self.assertTrue(fields[5][2].hidden) 175 | self.assertFalse(fields[5][2].modified) 176 | 177 | self.assertEqual(fields[6][0], 660) 178 | self.assertEqual(fields[6][1], 669) 179 | 180 | self.assertEqual(fields[7][0], 680) 181 | self.assertEqual(fields[7][1], 689) 182 | 183 | self.assertEqual(fields[8][0], 700) 184 | self.assertEqual(fields[8][1], 709) 185 | 186 | self.assertEqual(fields[9][0], 740) 187 | self.assertEqual(fields[9][1], 749) 188 | self.assertFalse(fields[9][2].protected) 189 | self.assertFalse(fields[9][2].numeric) 190 | self.assertFalse(fields[9][2].intensified) 191 | self.assertFalse(fields[9][2].hidden) 192 | self.assertTrue(fields[9][2].modified) 193 | 194 | self.assertEqual(fields[10][0], 760) 195 | self.assertEqual(fields[10][1], 769) 196 | self.assertFalse(fields[10][2].protected) 197 | self.assertFalse(fields[10][2].numeric) 198 | self.assertTrue(fields[10][2].intensified) 199 | self.assertFalse(fields[10][2].hidden) 200 | self.assertTrue(fields[11][2].modified) 201 | 202 | self.assertEqual(fields[11][0], 780) 203 | self.assertEqual(fields[11][1], 789) 204 | self.assertFalse(fields[11][2].protected) 205 | self.assertFalse(fields[11][2].numeric) 206 | self.assertFalse(fields[11][2].intensified) 207 | self.assertTrue(fields[11][2].hidden) 208 | self.assertTrue(fields[11][2].modified) 209 | 210 | self.assertEqual(fields[12][0], 900) 211 | self.assertEqual(fields[12][1], 909) 212 | 213 | def test_erase_write_screen2(self): 214 | # Arrange 215 | self.stream.read_multiple = Mock(return_value=[(SCREEN2, None)]) 216 | 217 | # Act 218 | self.emulator.update() 219 | 220 | # Assert 221 | self.assertFalse(self.emulator.alternate) 222 | 223 | fields = self.emulator.get_fields(protected=False) 224 | 225 | self.assertEqual(len(fields), 1) 226 | 227 | self.assertEqual(fields[0][0], 1899) 228 | self.assertEqual(fields[0][1], 20) 229 | self.assertFalse(fields[0][2].protected) 230 | self.assertFalse(fields[0][2].numeric) 231 | self.assertFalse(fields[0][2].intensified) 232 | self.assertFalse(fields[0][2].hidden) 233 | self.assertFalse(fields[0][2].modified) 234 | 235 | def test_read_modified(self): 236 | # Arrange 237 | self.stream.read_multiple = Mock(side_effect=[[(SCREEN1, None)], [(bytes.fromhex('06'), None)]]) 238 | 239 | self.emulator.update() 240 | 241 | self.emulator.cursor_address = 505 242 | 243 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 244 | self.emulator.input(character) 245 | 246 | self.assertEqual(self.emulator.cursor_address, 525) 247 | 248 | self.emulator.aid(AID.ENTER) 249 | 250 | self.stream.write.reset_mock() 251 | 252 | # Act 253 | self.emulator.update() 254 | 255 | # Assert 256 | self.stream.write.assert_called_with(bytes.fromhex('7dc84d11c7f4c1c2c3c4c511c8c8c6c7c8c9d1114be4e7e7e7e7e7114bf8e7e7e7e7e7114c4ce7e7e7e7e7')) 257 | 258 | def test_erase_write_alternate_screen1(self): 259 | # Arrange 260 | self.stream.read_multiple = Mock(return_value=[(bytes([0x0d, *SCREEN1[1:]]), None)]) 261 | 262 | # Act 263 | self.emulator.update() 264 | 265 | # Assert 266 | self.assertTrue(self.emulator.alternate) 267 | 268 | fields = self.emulator.get_fields(protected=False) 269 | 270 | self.assertEqual(len(fields), 13) 271 | 272 | def test_read_modified_all(self): 273 | # Arrange 274 | self.stream.read_multiple = Mock(side_effect=[[(SCREEN1, None)], [(bytes.fromhex('0e'), None)]]) 275 | 276 | self.emulator.update() 277 | 278 | self.emulator.cursor_address = 505 279 | 280 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 281 | self.emulator.input(character) 282 | 283 | self.assertEqual(self.emulator.cursor_address, 525) 284 | 285 | self.emulator.aid(AID.PA1) 286 | 287 | self.stream.write.reset_mock() 288 | 289 | # Act 290 | self.emulator.update() 291 | 292 | # Assert 293 | self.stream.write.assert_called_with(bytes.fromhex('6cc84d11c7f4c1c2c3c4c511c8c8c6c7c8c9d1114be4e7e7e7e7e7114bf8e7e7e7e7e7114c4ce7e7e7e7e7')) 294 | 295 | def test_erase_all_unprotected(self): 296 | # Arrange 297 | self.stream.read_multiple = Mock(side_effect=[[(SCREEN1, None)], [(bytes.fromhex('0f'), None)]]) 298 | 299 | self.emulator.update() 300 | 301 | self.emulator.cursor_address = 505 302 | 303 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 304 | self.emulator.input(character) 305 | 306 | self.emulator.current_aid = AID.ENTER 307 | self.emulator.keyboard_locked = True 308 | 309 | self.assertEqual(self.emulator.cursor_address, 525) 310 | 311 | fields = self.emulator.get_fields(protected=False) 312 | 313 | self.assertTrue(fields[0][2].modified) 314 | self.assertEqual(self.emulator.get_bytes(fields[0][0], fields[0][1]), bytes.fromhex('0000000000c1c2c3c4c5')) 315 | 316 | self.assertTrue(fields[1][2].modified) 317 | self.assertEqual(self.emulator.get_bytes(fields[1][0], fields[1][1]), bytes.fromhex('c6c7c8c9d10000000000')) 318 | 319 | # Act 320 | self.emulator.update() 321 | 322 | # Assert 323 | fields = self.emulator.get_fields(protected=False) 324 | 325 | self.assertFalse(fields[0][2].modified) 326 | self.assertEqual(self.emulator.get_bytes(fields[0][0], fields[0][1]), bytes.fromhex('00000000000000000000')) 327 | 328 | self.assertFalse(fields[1][2].modified) 329 | self.assertEqual(self.emulator.get_bytes(fields[1][0], fields[1][1]), bytes.fromhex('00000000000000000000')) 330 | 331 | self.assertEqual(self.emulator.current_aid, AID.NONE) 332 | self.assertFalse(self.emulator.keyboard_locked) 333 | 334 | def test_write_structured_field_read_partition_query(self): 335 | # Arrange 336 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('11 00 05 01 ff 02'), None)]) 337 | 338 | # Act 339 | self.emulator.update() 340 | 341 | # Assert 342 | self.stream.write.assert_called_with(bytes.fromhex('88 00 0b 81 80 80 81 84 86 87 88 a6 00 17 81 81 01 00 00 50 00 2b 01 00 0a 02 e5 00 02 00 6f 09 0c 0d 70 00 08 81 84 01 0d 70 00 00 16 81 86 00 08 00 f4 f1 f1 f2 f2 f3 f3 f4 f4 f5 f5 f6 f6 f7 f7 00 0d 81 87 04 00 f0 f1 f1 f2 f2 f4 f4 00 06 81 88 00 01 00 11 81 a6 00 00 0b 01 00 00 50 00 18 00 50 00 2b')) 343 | 344 | def test_tn3270e_successful_with_no_response_flag(self): 345 | # Arrange 346 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.NO, 123))]) 347 | 348 | # Act 349 | self.emulator.update() 350 | 351 | # Assert 352 | self.stream.send_tn3270e_positive_response.assert_not_called() 353 | 354 | def test_tn3270e_error_with_no_response_flag(self): 355 | # Arrange 356 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.NO, 123))]) 357 | 358 | self.emulator._execute = Mock(side_effect=Exception('Error')) 359 | 360 | # Act 361 | with self.assertRaises(Exception): 362 | self.emulator.update() 363 | 364 | # Assert 365 | self.stream.send_tn3270e_negative_response.assert_not_called() 366 | 367 | def test_tn3270e_successful_with_error_response_flag(self): 368 | # Arrange 369 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.ERROR, 123))]) 370 | 371 | # Act 372 | self.emulator.update() 373 | 374 | # Assert 375 | self.stream.send_tn3270e_positive_response.assert_not_called() 376 | 377 | def test_tn3270e_error_with_error_response_flag(self): 378 | # Arrange 379 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.ERROR, 123))]) 380 | 381 | self.emulator._execute = Mock(side_effect=Exception('Error')) 382 | 383 | # Act 384 | with self.assertRaises(Exception): 385 | self.emulator.update() 386 | 387 | # Assert 388 | self.stream.send_tn3270e_negative_response.assert_called_once_with(123, 0) 389 | 390 | def test_tn3270e_successful_with_always_response_flag(self): 391 | # Arrange 392 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.ALWAYS, 123))]) 393 | 394 | # Act 395 | self.emulator.update() 396 | 397 | # Assert 398 | self.stream.send_tn3270e_positive_response.assert_called_once_with(123) 399 | 400 | def test_tn3270e_error_with_always_response_flag(self): 401 | # Arrange 402 | self.stream.read_multiple = Mock(return_value=[(bytes.fromhex('01 c3 11 4b f0 1d f8 c8 c5 d3 d3 d6 40 e6 d6 d9 d3 c4'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.ALWAYS, 123))]) 403 | 404 | self.emulator._execute = Mock(side_effect=Exception('Error')) 405 | 406 | # Act 407 | with self.assertRaises(Exception): 408 | self.emulator.update() 409 | 410 | # Assert 411 | self.stream.send_tn3270e_negative_response.assert_called_once_with(123, 0) 412 | 413 | class AidTestCase(unittest.TestCase): 414 | def setUp(self): 415 | self.stream = create_autospec(Telnet, instance=True) 416 | 417 | self.stream.write = Mock() 418 | 419 | self.emulator = Emulator(self.stream, 24, 80) 420 | 421 | def test_screen1_short_read(self): 422 | # Arrange 423 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 424 | 425 | self.emulator.update() 426 | 427 | self.emulator.cursor_address = 500 428 | 429 | for character in 'ABCDEFGHIJKLMNO'.encode('ibm037'): 430 | self.emulator.input(character) 431 | 432 | self.assertEqual(self.emulator.cursor_address, 525) 433 | 434 | # Act 435 | self.emulator.aid(AID.PA1) 436 | 437 | # Assert 438 | self.stream.write.assert_called_with(bytes.fromhex('6c')) 439 | 440 | def test_screen1_long_read(self): 441 | # Arrange 442 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 443 | 444 | self.emulator.update() 445 | 446 | self.emulator.cursor_address = 505 447 | 448 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 449 | self.emulator.input(character) 450 | 451 | self.assertEqual(self.emulator.cursor_address, 525) 452 | 453 | # Act 454 | self.emulator.aid(AID.ENTER) 455 | 456 | # Assert 457 | self.stream.write.assert_called_with(bytes.fromhex('7dc84d11c7f4c1c2c3c4c511c8c8c6c7c8c9d1114be4e7e7e7e7e7114bf8e7e7e7e7e7114c4ce7e7e7e7e7')) 458 | 459 | def test_screen2_long_read(self): 460 | # Arrange 461 | self.stream.read_multiple = Mock(return_value=[(SCREEN2, None)]) 462 | 463 | self.emulator.update() 464 | 465 | self.emulator.cursor_address = 0 466 | 467 | for character in (string.ascii_uppercase + string.ascii_lowercase).encode('ibm037'): 468 | self.emulator.input(character) 469 | 470 | self.assertEqual(self.emulator.cursor_address, 10) 471 | 472 | # Act 473 | self.emulator.aid(AID.ENTER) 474 | 475 | # Assert 476 | self.stream.write.assert_called_with(bytes.fromhex('7d404a115d6be5e6e7e8e9818283848586878889919293949596979899a2a3a4a5a6a7a8a9d2d3d4d5d6d7d8d9e2e3e4')) 477 | 478 | def test_clear(self): 479 | # Arrange 480 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 481 | 482 | self.emulator.update() 483 | 484 | self.emulator.cursor_address = 505 485 | 486 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 487 | self.emulator.input(character) 488 | 489 | self.assertEqual(self.emulator.cursor_address, 525) 490 | 491 | # Act 492 | self.emulator.aid(AID.CLEAR) 493 | 494 | # Assert 495 | self.stream.write.assert_called_with(bytes.fromhex('6d')) 496 | 497 | self.assertEqual(self.emulator.address, 0) 498 | 499 | self.assertTrue(all([isinstance(cell, CharacterCell) and cell.byte == 0x00 for cell in self.emulator.cells])) 500 | 501 | self.assertEqual(self.emulator.cursor_address, 0) 502 | 503 | class TabTestCase(unittest.TestCase): 504 | def setUp(self): 505 | self.stream = create_autospec(Telnet, instance=True) 506 | 507 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 508 | 509 | self.emulator = Emulator(self.stream, 24, 80) 510 | 511 | self.emulator.update() 512 | 513 | self.emulator.cursor_address = 0 514 | 515 | def test_blank_screen(self): 516 | # Arrange 517 | self.emulator = Emulator(None, 24, 80) 518 | 519 | self.assertEqual(self.emulator.cursor_address, 0) 520 | 521 | # Act 522 | self.emulator.tab() 523 | 524 | # Assert 525 | self.assertEqual(self.emulator.cursor_address, 0) 526 | 527 | def test_forward(self): 528 | for address in [0, 498, 499]: 529 | with self.subTest(address=address): 530 | # Arrange 531 | self.emulator.cursor_address = address 532 | 533 | # Act 534 | self.emulator.tab() 535 | 536 | # Assert 537 | self.assertEqual(self.emulator.cursor_address, 500) 538 | 539 | def test_forward_to_next_field(self): 540 | # Arrange 541 | self.emulator.cursor_address = 500 542 | 543 | # Act 544 | self.emulator.tab() 545 | 546 | # Assert 547 | self.assertEqual(self.emulator.cursor_address, 520) 548 | 549 | def test_backward(self): 550 | for address in [520, 519, 510]: 551 | with self.subTest(address=address): 552 | # Arrange 553 | self.emulator.cursor_address = address 554 | 555 | # Act 556 | self.emulator.tab(direction=-1) 557 | 558 | # Assert 559 | self.assertEqual(self.emulator.cursor_address, 500) 560 | 561 | def test_backward_to_start_of_field(self): 562 | # Arrange 563 | self.emulator.cursor_address = 505 564 | 565 | # Act 566 | self.emulator.tab(direction=-1) 567 | 568 | # Assert 569 | self.assertEqual(self.emulator.cursor_address, 500) 570 | 571 | def test_wrap_forward(self): 572 | # Arrange 573 | self.emulator.cursor_address = 900 574 | 575 | # Act 576 | self.emulator.tab() 577 | 578 | # Assert 579 | self.assertEqual(self.emulator.cursor_address, 500) 580 | 581 | def test_wrap_backward(self): 582 | # Arrange 583 | self.emulator.cursor_address = 500 584 | 585 | # Act 586 | self.emulator.tab(direction=-1) 587 | 588 | # Assert 589 | self.assertEqual(self.emulator.cursor_address, 900) 590 | 591 | class NewlineTestCase(unittest.TestCase): 592 | def setUp(self): 593 | self.stream = create_autospec(Telnet, instance=True) 594 | 595 | self.stream.read_multiple = Mock(return_value=[(SCREEN3, None)]) 596 | 597 | self.emulator = Emulator(self.stream, 24, 80) 598 | 599 | self.emulator.update() 600 | 601 | self.emulator.cursor_address = 0 602 | 603 | def test_blank_screen(self): 604 | # Arrange 605 | self.emulator = Emulator(None, 24, 80) 606 | 607 | self.assertEqual(self.emulator.cursor_address, 0) 608 | 609 | # Act 610 | self.emulator.newline() 611 | 612 | # Assert 613 | self.assertEqual(self.emulator.cursor_address, 0) 614 | 615 | def test_next_line(self): 616 | for address in [0, 1, 20]: 617 | with self.subTest(address=address): 618 | # Arrange 619 | self.emulator.cursor_address = address 620 | 621 | # Act 622 | self.emulator.newline() 623 | 624 | # Assert 625 | self.assertEqual(self.emulator.cursor_address, 80) 626 | 627 | def test_first_field_on_next_line(self): 628 | # Arrange 629 | self.emulator.cursor_address = 400 630 | 631 | # Act 632 | self.emulator.newline() 633 | 634 | # Assert 635 | self.assertEqual(self.emulator.cursor_address, 504) 636 | 637 | def test_wrap(self): 638 | # Arrange 639 | self.emulator.cursor_address = 504 640 | 641 | # Act 642 | self.emulator.newline() 643 | 644 | # Assert 645 | self.assertEqual(self.emulator.cursor_address, 4) 646 | 647 | class HomeTestCase(unittest.TestCase): 648 | def setUp(self): 649 | self.stream = create_autospec(Telnet, instance=True) 650 | 651 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 652 | 653 | self.emulator = Emulator(self.stream, 24, 80) 654 | 655 | self.emulator.update() 656 | 657 | self.emulator.cursor_address = 0 658 | 659 | def test_blank_screen(self): 660 | # Arrange 661 | self.emulator = Emulator(None, 24, 80) 662 | 663 | self.assertEqual(self.emulator.cursor_address, 0) 664 | 665 | # Act 666 | self.emulator.home() 667 | 668 | # Assert 669 | self.assertEqual(self.emulator.cursor_address, 0) 670 | 671 | def test(self): 672 | for address in [0, 498, 499, 500, 505, 510]: 673 | with self.subTest(address=address): 674 | # Arrange 675 | self.emulator.cursor_address = address 676 | 677 | # Act 678 | self.emulator.home() 679 | 680 | # Assert 681 | self.assertEqual(self.emulator.cursor_address, 500) 682 | 683 | class CursorUpTestCase(unittest.TestCase): 684 | def setUp(self): 685 | self.emulator = Emulator(None, 24, 80) 686 | 687 | def test_first_row(self): 688 | # Arrange 689 | self.emulator.cursor_address = 20 690 | 691 | # Act 692 | self.emulator.cursor_up() 693 | 694 | # Assert 695 | self.assertEqual(self.emulator.cursor_address, 1860) 696 | 697 | def test_last_row(self): 698 | # Arrange 699 | self.emulator.cursor_address = 1860 700 | 701 | # Act 702 | self.emulator.cursor_up() 703 | 704 | # Assert 705 | self.assertEqual(self.emulator.cursor_address, 1780) 706 | 707 | class CursorDownTestCase(unittest.TestCase): 708 | def setUp(self): 709 | self.emulator = Emulator(None, 24, 80) 710 | 711 | def test_first_row(self): 712 | # Arrange 713 | self.emulator.cursor_address = 20 714 | 715 | # Act 716 | self.emulator.cursor_down() 717 | 718 | # Assert 719 | self.assertEqual(self.emulator.cursor_address, 100) 720 | 721 | def test_last_row(self): 722 | # Arrange 723 | self.emulator.cursor_address = 1860 724 | 725 | # Act 726 | self.emulator.cursor_down() 727 | 728 | # Assert 729 | self.assertEqual(self.emulator.cursor_address, 20) 730 | 731 | class CursorLeftTestCase(unittest.TestCase): 732 | def setUp(self): 733 | self.emulator = Emulator(None, 24, 80) 734 | 735 | def test_first_cell(self): 736 | # Arrange 737 | self.emulator.cursor_address = 0 738 | 739 | # Act 740 | self.emulator.cursor_left() 741 | 742 | # Assert 743 | self.assertEqual(self.emulator.cursor_address, 1919) 744 | 745 | def test_last_cell(self): 746 | # Arrange 747 | self.emulator.cursor_address = 1919 748 | 749 | # Act 750 | self.emulator.cursor_left() 751 | 752 | # Assert 753 | self.assertEqual(self.emulator.cursor_address, 1918) 754 | 755 | def test_fast(self): 756 | # Arrange 757 | self.emulator.cursor_address = 1919 758 | 759 | # Act 760 | self.emulator.cursor_left(2) 761 | 762 | # Assert 763 | self.assertEqual(self.emulator.cursor_address, 1917) 764 | 765 | def test_invalid_rate(self): 766 | for rate in [-1, 0, 3]: 767 | with self.subTest(rate=rate): 768 | with self.assertRaisesRegex(ValueError, 'Invalid rate'): 769 | self.emulator.cursor_left(rate) 770 | 771 | class CursorRightTestCase(unittest.TestCase): 772 | def setUp(self): 773 | self.emulator = Emulator(None, 24, 80) 774 | 775 | def test_first_cell(self): 776 | # Arrange 777 | self.emulator.cursor_address = 0 778 | 779 | # Act 780 | self.emulator.cursor_right() 781 | 782 | # Assert 783 | self.assertEqual(self.emulator.cursor_address, 1) 784 | 785 | def test_last_cell(self): 786 | # Arrange 787 | self.emulator.cursor_address = 1919 788 | 789 | # Act 790 | self.emulator.cursor_right() 791 | 792 | # Assert 793 | self.assertEqual(self.emulator.cursor_address, 0) 794 | 795 | def test_fast(self): 796 | # Arrange 797 | self.emulator.cursor_address = 0 798 | 799 | # Act 800 | self.emulator.cursor_right(2) 801 | 802 | # Assert 803 | self.assertEqual(self.emulator.cursor_address, 2) 804 | 805 | def test_invalid_rate(self): 806 | for rate in [-1, 0, 3]: 807 | with self.subTest(rate=rate): 808 | with self.assertRaisesRegex(ValueError, 'Invalid rate'): 809 | self.emulator.cursor_right(rate) 810 | 811 | class InputTestCase(unittest.TestCase): 812 | def setUp(self): 813 | self.stream = create_autospec(Telnet, instance=True) 814 | 815 | self.emulator = Emulator(self.stream, 24, 80) 816 | 817 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 818 | 819 | self.emulator.update() 820 | 821 | def test_attribute_cell(self): 822 | # Arrange 823 | self.emulator.cursor_address = 499 824 | 825 | # Act and assert 826 | with self.assertRaises(ProtectedCellOperatorError): 827 | self.emulator.input(0xe7) 828 | 829 | def test_protected_cell(self): 830 | # Arrange 831 | self.emulator.cursor_address = 420 832 | 833 | # Act and assert 834 | with self.assertRaises(ProtectedCellOperatorError): 835 | self.emulator.input(0xe7) 836 | 837 | def test_alphanumeric(self): 838 | # Arrange 839 | self.emulator.cursor_address = 500 840 | 841 | self.assertFalse(self.emulator.cells[499].attribute.modified) 842 | 843 | # Act 844 | self.emulator.input(0xe7) 845 | 846 | # Assert 847 | self.assertEqual(self.emulator.cursor_address, 501) 848 | self.assertTrue(self.emulator.cells[499].attribute.modified) 849 | self.assertEqual(self.emulator.cells[500].byte, 0xe7) 850 | 851 | def test_skip(self): 852 | # Arrange 853 | self.emulator.cursor_address = 500 854 | 855 | self.assertTrue(self.emulator.cells[510].attribute.skip) 856 | 857 | # Act 858 | for _ in range(10): 859 | self.emulator.input(0xe7) 860 | 861 | # Assert 862 | self.assertEqual(self.emulator.cursor_address, 520) 863 | 864 | def test_no_skip(self): 865 | # Arrange 866 | self.emulator.cursor_address = 900 867 | 868 | self.assertFalse(self.emulator.cells[910].attribute.skip) 869 | 870 | # Act 871 | for _ in range(10): 872 | self.emulator.input(0xe7) 873 | 874 | # Assert 875 | self.assertEqual(self.emulator.cursor_address, 911) 876 | 877 | def test_wrap(self): 878 | # Arrange 879 | self.stream = create_autospec(Telnet, instance=True) 880 | 881 | self.emulator = Emulator(self.stream, 24, 80) 882 | 883 | self.stream.read_multiple = Mock(return_value=[(SCREEN2, None)]) 884 | 885 | self.emulator.update() 886 | 887 | fields = self.emulator.get_fields(protected=False) 888 | 889 | self.assertEqual(len(fields), 1) 890 | 891 | self.assertEqual(fields[0][0], 1899) 892 | self.assertEqual(fields[0][1], 20) 893 | 894 | self.assertEqual(self.emulator.cursor_address, 0) 895 | 896 | # Act 897 | for character in (string.ascii_uppercase + string.ascii_lowercase).encode('ibm037'): 898 | self.emulator.input(character) 899 | 900 | # Assert 901 | self.assertEqual(self.emulator.cursor_address, 10) 902 | 903 | text = self.emulator.get_bytes(fields[0][0], fields[0][1]).decode('ibm037') 904 | 905 | self.assertEqual(text, 'VWXYZabcdefghijklmnopqrstuvwxyzKLMNOPQRSTU') 906 | 907 | def test_insert(self): 908 | # Arrange 909 | self.emulator.cursor_address = 500 910 | 911 | self.emulator.input(0xc1) 912 | self.emulator.input(0xc4) 913 | self.emulator.input(0xc5) 914 | 915 | self.emulator.cursor_address = 504 916 | 917 | self.emulator.input(0xc6) 918 | self.emulator.input(0xc7) 919 | self.emulator.input(0x40) 920 | self.emulator.input(0xc8) 921 | 922 | self.assertEqual(self.emulator.get_bytes(500, 509), bytes.fromhex('c1 c4 c5 00 c6 c7 40 c8 00 00')) 923 | 924 | self.emulator.cursor_address = 501 925 | 926 | # Act 927 | self.emulator.input(0xc2, insert=True) 928 | self.emulator.input(0xc3, insert=True) 929 | 930 | # Assert 931 | self.assertEqual(self.emulator.cursor_address, 503) 932 | 933 | self.assertEqual(self.emulator.get_bytes(500, 509), bytes.fromhex('c1 c2 c3 c4 c5 c6 c7 40 c8 00')) 934 | 935 | def test_insert_overflow(self): 936 | # Arrange 937 | self.emulator.cursor_address = 505 938 | 939 | self.emulator.input(0xc1) 940 | self.emulator.input(0xc2) 941 | self.emulator.input(0xc3) 942 | self.emulator.input(0xc4) 943 | self.emulator.input(0xc5) 944 | 945 | self.emulator.cursor_address = 505 946 | 947 | # Act and assert 948 | with self.assertRaises(FieldOverflowOperatorError): 949 | self.emulator.input(0xc1, insert=True) 950 | 951 | class DupTestCase(unittest.TestCase): 952 | def setUp(self): 953 | self.stream = create_autospec(Telnet, instance=True) 954 | 955 | self.emulator = Emulator(self.stream, 24, 80) 956 | 957 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 958 | 959 | self.emulator.update() 960 | 961 | def test_attribute_cell(self): 962 | # Arrange 963 | self.emulator.cursor_address = 499 964 | 965 | # Act and assert 966 | with self.assertRaises(ProtectedCellOperatorError): 967 | self.emulator.dup() 968 | 969 | def test_protected_cell(self): 970 | # Arrange 971 | self.emulator.cursor_address = 420 972 | 973 | # Act and assert 974 | with self.assertRaises(ProtectedCellOperatorError): 975 | self.emulator.dup() 976 | 977 | def test_first_field_character(self): 978 | # Arrange 979 | self.emulator.cursor_address = 500 980 | 981 | # Act 982 | self.emulator.dup() 983 | 984 | # Assert 985 | self.assertEqual(self.emulator.cursor_address, 520) 986 | self.assertTrue(self.emulator.cells[499].attribute.modified) 987 | self.assertEqual(self.emulator.cells[500].byte, 0x1c) 988 | 989 | class BackspaceTestCase(unittest.TestCase): 990 | def setUp(self): 991 | self.stream = create_autospec(Telnet, instance=True) 992 | 993 | self.emulator = Emulator(self.stream, 24, 80) 994 | 995 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 996 | 997 | self.emulator.update() 998 | 999 | def test_attribute_cell(self): 1000 | # Arrange 1001 | self.emulator.cursor_address = 499 1002 | 1003 | # Act and assert 1004 | with self.assertRaises(ProtectedCellOperatorError): 1005 | self.emulator.backspace() 1006 | 1007 | def test_protected_cell(self): 1008 | # Arrange 1009 | self.emulator.cursor_address = 420 1010 | 1011 | # Act and assert 1012 | with self.assertRaises(ProtectedCellOperatorError): 1013 | self.emulator.backspace() 1014 | 1015 | def test_first_field_character(self): 1016 | # Arrange 1017 | self.emulator.cursor_address = 660 1018 | 1019 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1020 | 1021 | # Act 1022 | self.emulator.backspace() 1023 | 1024 | # Assert 1025 | self.assertEqual(self.emulator.cursor_address, 660) 1026 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1027 | self.assertEqual(self.emulator.cells[660].byte, 0xe7) 1028 | 1029 | def test_from_middle_to_start(self): 1030 | # Arrange 1031 | address = 660 1032 | 1033 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1034 | self.emulator.cells[address].byte = character 1035 | 1036 | address += 1 1037 | 1038 | self.emulator.cursor_address = 665 1039 | 1040 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1041 | 1042 | # Act 1043 | for _ in range(5): 1044 | self.emulator.backspace() 1045 | 1046 | # Assert 1047 | self.assertEqual(self.emulator.cursor_address, 660) 1048 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1049 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('c6c7c8c9d10000000000')) 1050 | 1051 | def test_from_end_to_start(self): 1052 | # Arrange 1053 | address = 660 1054 | 1055 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1056 | self.emulator.cells[address].byte = character 1057 | 1058 | address += 1 1059 | 1060 | self.emulator.cursor_address = 669 1061 | 1062 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1063 | 1064 | # Act 1065 | for _ in range(10): 1066 | self.emulator.backspace() 1067 | 1068 | # Assert 1069 | self.assertEqual(self.emulator.cursor_address, 660) 1070 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1071 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('d1000000000000000000')) 1072 | 1073 | class DeleteTestCase(unittest.TestCase): 1074 | def setUp(self): 1075 | self.stream = create_autospec(Telnet, instance=True) 1076 | 1077 | self.emulator = Emulator(self.stream, 24, 80) 1078 | 1079 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 1080 | 1081 | self.emulator.update() 1082 | 1083 | def test_attribute_cell(self): 1084 | # Arrange 1085 | self.emulator.cursor_address = 499 1086 | 1087 | # Act and assert 1088 | with self.assertRaises(ProtectedCellOperatorError): 1089 | self.emulator.delete() 1090 | 1091 | def test_protected_cell(self): 1092 | # Arrange 1093 | self.emulator.cursor_address = 420 1094 | 1095 | # Act and assert 1096 | with self.assertRaises(ProtectedCellOperatorError): 1097 | self.emulator.delete() 1098 | 1099 | def test_from_middle_to_end(self): 1100 | # Arrange 1101 | address = 660 1102 | 1103 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1104 | self.emulator.cells[address].byte = character 1105 | 1106 | address += 1 1107 | 1108 | self.emulator.cursor_address = 665 1109 | 1110 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1111 | 1112 | # Act 1113 | for _ in range(5): 1114 | self.emulator.delete() 1115 | 1116 | # Assert 1117 | self.assertEqual(self.emulator.cursor_address, 665) 1118 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1119 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('c1c2c3c4c50000000000')) 1120 | 1121 | def test_from_start_to_end(self): 1122 | # Arrange 1123 | address = 660 1124 | 1125 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1126 | self.emulator.cells[address].byte = character 1127 | 1128 | address += 1 1129 | 1130 | self.emulator.cursor_address = 660 1131 | 1132 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1133 | 1134 | # Act 1135 | for _ in range(10): 1136 | self.emulator.delete() 1137 | 1138 | # Assert 1139 | self.assertEqual(self.emulator.cursor_address, 660) 1140 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1141 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('00000000000000000000')) 1142 | 1143 | class EraseEndOfFieldTestCase(unittest.TestCase): 1144 | def setUp(self): 1145 | self.stream = create_autospec(Telnet, instance=True) 1146 | 1147 | self.emulator = Emulator(self.stream, 24, 80) 1148 | 1149 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 1150 | 1151 | self.emulator.update() 1152 | 1153 | def test_attribute_cell(self): 1154 | # Arrange 1155 | self.emulator.cursor_address = 499 1156 | 1157 | # Act and assert 1158 | with self.assertRaises(ProtectedCellOperatorError): 1159 | self.emulator.erase_end_of_field() 1160 | 1161 | def test_protected_cell(self): 1162 | # Arrange 1163 | self.emulator.cursor_address = 420 1164 | 1165 | # Act and assert 1166 | with self.assertRaises(ProtectedCellOperatorError): 1167 | self.emulator.erase_end_of_field() 1168 | 1169 | def test_from_middle(self): 1170 | # Arrange 1171 | address = 660 1172 | 1173 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1174 | self.emulator.cells[address].byte = character 1175 | 1176 | address += 1 1177 | 1178 | self.emulator.cursor_address = 665 1179 | 1180 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1181 | 1182 | # Act 1183 | self.emulator.erase_end_of_field() 1184 | 1185 | # Assert 1186 | self.assertEqual(self.emulator.cursor_address, 665) 1187 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1188 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('c1c2c3c4c50000000000')) 1189 | 1190 | def test_from_start(self): 1191 | # Arrange 1192 | address = 660 1193 | 1194 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1195 | self.emulator.cells[address].byte = character 1196 | 1197 | address += 1 1198 | 1199 | self.emulator.cursor_address = 660 1200 | 1201 | self.assertFalse(self.emulator.cells[659].attribute.modified) 1202 | 1203 | # Act 1204 | self.emulator.erase_end_of_field() 1205 | 1206 | # Assert 1207 | self.assertEqual(self.emulator.cursor_address, 660) 1208 | self.assertTrue(self.emulator.cells[659].attribute.modified) 1209 | self.assertEqual(self.emulator.get_bytes(660, 669), bytes.fromhex('00000000000000000000')) 1210 | 1211 | class EraseInputTestCase(unittest.TestCase): 1212 | def test(self): 1213 | # Arrange 1214 | stream = create_autospec(Telnet, instance=True) 1215 | 1216 | emulator = Emulator(stream, 24, 80) 1217 | 1218 | stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 1219 | 1220 | emulator.update() 1221 | 1222 | emulator.cursor_address = 505 1223 | 1224 | for character in 'ABCDEFGHIJ'.encode('ibm037'): 1225 | emulator.input(character) 1226 | 1227 | fields = emulator.get_fields(protected=False) 1228 | 1229 | self.assertTrue(fields[0][2].modified) 1230 | self.assertEqual(emulator.get_bytes(fields[0][0], fields[0][1]), bytes.fromhex('0000000000c1c2c3c4c5')) 1231 | 1232 | self.assertTrue(fields[1][2].modified) 1233 | self.assertEqual(emulator.get_bytes(fields[1][0], fields[1][1]), bytes.fromhex('c6c7c8c9d10000000000')) 1234 | 1235 | # Act 1236 | emulator.erase_input() 1237 | 1238 | # Assert 1239 | fields = emulator.get_fields(protected=False) 1240 | 1241 | self.assertFalse(fields[0][2].modified) 1242 | self.assertEqual(emulator.get_bytes(fields[0][0], fields[0][1]), bytes.fromhex('00000000000000000000')) 1243 | 1244 | self.assertFalse(fields[1][2].modified) 1245 | self.assertEqual(emulator.get_bytes(fields[1][0], fields[1][1]), bytes.fromhex('00000000000000000000')) 1246 | 1247 | class IsFormattedTestCase(unittest.TestCase): 1248 | def setUp(self): 1249 | self.stream = create_autospec(Telnet, instance=True) 1250 | 1251 | self.emulator = Emulator(self.stream, 24, 80) 1252 | 1253 | def test_formatted(self): 1254 | # Arrange 1255 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 1256 | 1257 | self.emulator.update() 1258 | 1259 | # Act and assert 1260 | self.assertTrue(self.emulator.is_formatted()) 1261 | 1262 | def test_unformatted(self): 1263 | # Arrange 1264 | self.stream.read_multiple = Mock(return_value=[(SCREEN4, None)]) 1265 | 1266 | self.emulator.update() 1267 | 1268 | # Act and assert 1269 | self.assertFalse(self.emulator.is_formatted()) 1270 | 1271 | class GetFieldsTestCase(unittest.TestCase): 1272 | def setUp(self): 1273 | self.stream = create_autospec(Telnet, instance=True) 1274 | 1275 | self.emulator = Emulator(self.stream, 24, 80) 1276 | 1277 | self.stream.read_multiple = Mock(return_value=[(SCREEN1, None)]) 1278 | 1279 | self.emulator.update() 1280 | 1281 | def test_all(self): 1282 | # Act 1283 | fields = self.emulator.get_fields() 1284 | 1285 | # Assert 1286 | self.assertEqual(len(fields), 30) 1287 | 1288 | def test_protected(self): 1289 | # Act 1290 | fields = self.emulator.get_fields(protected=True) 1291 | 1292 | # Assert 1293 | self.assertEqual(len(fields), 17) 1294 | 1295 | def test_unprotected(self): 1296 | # Act 1297 | fields = self.emulator.get_fields(protected=False) 1298 | 1299 | # Assert 1300 | self.assertEqual(len(fields), 13) 1301 | 1302 | def test_modified(self): 1303 | # Act 1304 | fields = self.emulator.get_fields(modified=True) 1305 | 1306 | # Assert 1307 | self.assertEqual(len(fields), 3) 1308 | 1309 | def test_unmodified(self): 1310 | # Act 1311 | fields = self.emulator.get_fields(modified=False) 1312 | 1313 | # Assert 1314 | self.assertEqual(len(fields), 27) 1315 | -------------------------------------------------------------------------------- /tests/test_telnet.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, create_autospec, patch 3 | 4 | from socket import socket 5 | import selectors 6 | from selectors import BaseSelector 7 | import ssl 8 | 9 | from tn3270.telnet import Telnet, TN3270EFunction, TN3270EMessageHeader, TN3270EDataType, TN3270EResponseFlag, encode_rfc1646_terminal_type, encode_rfc2355_device_type, decode_rfc2355_device_type 10 | 11 | class OpenTestCase(unittest.TestCase): 12 | def setUp(self): 13 | self.socket_mock = create_autospec(socket, instance=True) 14 | 15 | self.socket_selector_mock = create_autospec(BaseSelector, instance=True) 16 | 17 | selector_key = Mock(fileobj=self.socket_mock) 18 | 19 | self.socket_selector_mock.select.return_value = [(selector_key, selectors.EVENT_READ)] 20 | 21 | patcher = patch('socket.create_connection') 22 | 23 | create_connection_mock = patcher.start() 24 | 25 | create_connection_mock.return_value = self.socket_mock 26 | 27 | patcher = patch('selectors.DefaultSelector') 28 | 29 | default_selector_mock = patcher.start() 30 | 31 | default_selector_mock.return_value = self.socket_selector_mock 32 | 33 | patcher = patch('ssl.SSLContext.wrap_socket') 34 | 35 | ssl_wrap_socket_mock = patcher.start() 36 | 37 | ssl_wrap_socket_mock.return_value = self.socket_mock 38 | 39 | self.addCleanup(patch.stopall) 40 | 41 | def test_init(self): 42 | # Act 43 | self.telnet = Telnet('IBM-3279-2-E') 44 | 45 | # Assert 46 | self.assertFalse(self.telnet.is_tn3270_negotiated) 47 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 48 | 49 | def test_basic_tn3270_negotiation(self): 50 | # Arrange 51 | self.telnet = Telnet('IBM-3279-2-E') 52 | 53 | responses = [ 54 | bytes.fromhex('ff fd 18'), 55 | bytes.fromhex('ff fa 18 01 ff f0'), 56 | bytes.fromhex('ff fd 19'), 57 | bytes.fromhex('ff fb 19'), 58 | bytes.fromhex('ff fd 00'), 59 | bytes.fromhex('ff fb 00') 60 | ] 61 | 62 | self.socket_mock.recv = Mock(side_effect=responses) 63 | 64 | # Act 65 | self.telnet.open('mainframe', 23) 66 | 67 | # Assert 68 | self.assertTrue(self.telnet.is_tn3270_negotiated) 69 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 70 | 71 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 18')) 72 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 18 00 49 42 4d 2d 33 32 37 39 2d 32 2d 45 ff f0')) 73 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 19')) 74 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fd 19')) 75 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 00')) 76 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fd 00')) 77 | 78 | def test_tn3270e_negotiation(self): 79 | # Arrange 80 | self.telnet = Telnet('IBM-3279-2-E') 81 | 82 | responses = [ 83 | bytes.fromhex('ff fd 28'), 84 | bytes.fromhex('ff fa 28 08 02 ff f0'), 85 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 86 | bytes.fromhex('ff fa 28 03 04 ff f0') 87 | ] 88 | 89 | self.socket_mock.recv = Mock(side_effect=responses) 90 | 91 | # Act 92 | self.telnet.open('mainframe', 23) 93 | 94 | # Assert 95 | self.assertTrue(self.telnet.is_tn3270_negotiated) 96 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 97 | 98 | self.assertEqual(self.telnet.device_type, 'IBM-3278-2-E') 99 | self.assertEqual(self.telnet.device_name, 'TCP00034') 100 | 101 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 28')) 102 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 02 07 49 42 4d 2d 33 32 37 38 2d 32 2d 45 ff f0')) 103 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 ff f0')) 104 | 105 | def test_basic_tn3270_negotiation_when_tn3270e_not_enabled(self): 106 | # Arrange 107 | self.telnet = Telnet('IBM-3279-2-E', is_tn3270e_enabled=False) 108 | 109 | responses = [ 110 | bytes.fromhex('ff fd 28'), 111 | bytes.fromhex('ff fd 18'), 112 | bytes.fromhex('ff fa 18 01 ff f0'), 113 | bytes.fromhex('ff fd 19'), 114 | bytes.fromhex('ff fb 19'), 115 | bytes.fromhex('ff fd 00'), 116 | bytes.fromhex('ff fb 00') 117 | ] 118 | 119 | self.socket_mock.recv = Mock(side_effect=responses) 120 | 121 | # Act 122 | self.telnet.open('mainframe', 23) 123 | 124 | # Assert 125 | self.assertTrue(self.telnet.is_tn3270_negotiated) 126 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 127 | 128 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fc 28')) 129 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 18')) 130 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 18 00 49 42 4d 2d 33 32 37 39 2d 32 2d 45 ff f0')) 131 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 19')) 132 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fd 19')) 133 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 00')) 134 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fd 00')) 135 | 136 | def test_unsuccessful_negotiation(self): 137 | # Arrange 138 | self.telnet = Telnet('IBM-3279-2-E') 139 | 140 | self.socket_mock.recv = Mock(return_value='hello world'.encode('ascii')) 141 | 142 | # Act and assert 143 | with self.assertRaisesRegex(Exception, 'Unable to negotiate TN3270 mode'): 144 | self.telnet.open('mainframe', 23) 145 | 146 | def test_tn3270e_negotiation_ssl(self): 147 | # Arrange 148 | self.telnet = Telnet('IBM-3279-2-E') 149 | 150 | responses = [ 151 | bytes.fromhex('ff fd 28'), 152 | bytes.fromhex('ff fa 28 08 02 ff f0'), 153 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 154 | bytes.fromhex('ff fa 28 03 04 ff f0') 155 | ] 156 | 157 | self.socket_mock.recv = Mock(side_effect=responses) 158 | 159 | # Act 160 | ssl_context = ssl.create_default_context() 161 | self.telnet.open('mainframe', 23, ssl_context=ssl_context) 162 | 163 | # Assert 164 | self.assertTrue(self.telnet.is_tn3270_negotiated) 165 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 166 | 167 | self.assertEqual(self.telnet.device_type, 'IBM-3278-2-E') 168 | self.assertEqual(self.telnet.device_name, 'TCP00034') 169 | 170 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fb 28')) 171 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 02 07 49 42 4d 2d 33 32 37 38 2d 32 2d 45 ff f0')) 172 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 ff f0')) 173 | 174 | def test_tn3270_device_name_negotiation(self): 175 | # Arrange 176 | self.telnet = Telnet('IBM-3279-2-E') 177 | 178 | responses = [ 179 | bytes.fromhex('ff fd 18'), 180 | bytes.fromhex('ff fa 18 01 ff f0'), 181 | bytes.fromhex('ff fd 19'), 182 | bytes.fromhex('ff fb 19'), 183 | bytes.fromhex('ff fd 00'), 184 | bytes.fromhex('ff fb 00') 185 | ] 186 | 187 | self.socket_mock.recv = Mock(side_effect=responses) 188 | 189 | # Act 190 | self.telnet.open('mainframe', 23, ['LU1']) 191 | 192 | # Assert 193 | self.assertTrue(self.telnet.is_tn3270_negotiated) 194 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 195 | 196 | self.assertEqual(self.telnet.device_name, 'LU1') 197 | 198 | def test_tn3270_device_name_negotiation_second_device(self): 199 | # Arrange 200 | self.telnet = Telnet('IBM-3279-2-E') 201 | 202 | responses = [ 203 | bytes.fromhex('ff fd 18'), 204 | bytes.fromhex('ff fa 18 01 ff f0'), 205 | bytes.fromhex('ff fa 18 01 ff f0'), 206 | bytes.fromhex('ff fd 19'), 207 | bytes.fromhex('ff fb 19'), 208 | bytes.fromhex('ff fd 00'), 209 | bytes.fromhex('ff fb 00') 210 | ] 211 | 212 | self.socket_mock.recv = Mock(side_effect=responses) 213 | 214 | # Act 215 | self.telnet.open('mainframe', 23, ['LU1', 'LU2']) 216 | 217 | # Assert 218 | self.assertTrue(self.telnet.is_tn3270_negotiated) 219 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 220 | 221 | self.assertEqual(self.telnet.device_name, 'LU2') 222 | 223 | def test_tn3270_device_name_negotiation_exhausted(self): 224 | # Arrange 225 | self.telnet = Telnet('IBM-3279-2-E') 226 | 227 | responses = [ 228 | bytes.fromhex('ff fd 18'), 229 | bytes.fromhex('ff fa 18 01 ff f0'), 230 | bytes.fromhex('ff fa 18 01 ff f0'), 231 | bytes.fromhex('ff fa 18 01 ff f0'), 232 | bytes.fromhex('ff fd 19'), 233 | bytes.fromhex('ff fb 19'), 234 | bytes.fromhex('ff fd 00'), 235 | bytes.fromhex('ff fb 00') 236 | ] 237 | 238 | self.socket_mock.recv = Mock(side_effect=responses) 239 | 240 | # Act 241 | self.telnet.open('mainframe', 23, ['LU1', 'LU2']) 242 | 243 | # Assert 244 | self.assertTrue(self.telnet.is_tn3270_negotiated) 245 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 246 | 247 | self.assertIsNone(self.telnet.device_name) 248 | 249 | def test_tn3270e_device_name_negotiation(self): 250 | # Arrange 251 | self.telnet = Telnet('IBM-3279-2-E') 252 | 253 | responses = [ 254 | bytes.fromhex('ff fd 28'), 255 | bytes.fromhex('ff fa 28 08 02 ff f0'), 256 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 4c 55 31 ff f0'), 257 | bytes.fromhex('ff fa 28 03 04 ff f0') 258 | ] 259 | 260 | self.socket_mock.recv = Mock(side_effect=responses) 261 | 262 | # Act 263 | self.telnet.open('mainframe', 23, ['LU1']) 264 | 265 | # Assert 266 | self.assertTrue(self.telnet.is_tn3270_negotiated) 267 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 268 | 269 | self.assertEqual(self.telnet.device_name, 'LU1') 270 | 271 | def test_tn3270e_device_name_negotiation_second_device(self): 272 | # Arrange 273 | self.telnet = Telnet('IBM-3279-2-E') 274 | 275 | responses = [ 276 | bytes.fromhex('ff fd 28'), 277 | bytes.fromhex('ff fa 28 08 02 ff f0'), 278 | bytes.fromhex('ff fa 28 02 06 03 ff f0'), 279 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 4c 55 32 ff f0'), 280 | bytes.fromhex('ff fa 28 03 04 ff f0') 281 | ] 282 | 283 | self.socket_mock.recv = Mock(side_effect=responses) 284 | 285 | # Act 286 | self.telnet.open('mainframe', 23, ['LU1', 'LU2']) 287 | 288 | # Assert 289 | self.assertTrue(self.telnet.is_tn3270_negotiated) 290 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 291 | 292 | self.assertEqual(self.telnet.device_name, 'LU2') 293 | 294 | def test_device_name_negotiation_exhausted(self): 295 | # Arrange 296 | self.telnet = Telnet('IBM-3279-2-E') 297 | 298 | responses = [ 299 | bytes.fromhex('ff fd 28'), 300 | bytes.fromhex('ff fa 28 08 02 ff f0'), 301 | bytes.fromhex('ff fa 28 02 06 03 ff f0'), 302 | bytes.fromhex('ff fa 28 02 06 03 ff f0'), 303 | bytes.fromhex('ff fd 18'), 304 | bytes.fromhex('ff fa 18 01 ff f0'), 305 | bytes.fromhex('ff fa 18 01 ff f0'), 306 | bytes.fromhex('ff fa 18 01 ff f0'), 307 | bytes.fromhex('ff fd 19'), 308 | bytes.fromhex('ff fb 19'), 309 | bytes.fromhex('ff fd 00'), 310 | bytes.fromhex('ff fb 00') 311 | ] 312 | 313 | self.socket_mock.recv = Mock(side_effect=responses) 314 | 315 | # Act 316 | self.telnet.open('mainframe', 23, ['LU1', 'LU2']) 317 | 318 | # Assert 319 | self.assertTrue(self.telnet.is_tn3270_negotiated) 320 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 321 | 322 | self.assertIsNone(self.telnet.device_name) 323 | 324 | def test_tn3270e_function_negotiation_basic(self): 325 | # Arrange 326 | self.telnet = Telnet('IBM-3279-2-E', tn3270e_functions=[]) 327 | 328 | responses = [ 329 | bytes.fromhex('ff fd 28'), 330 | bytes.fromhex('ff fa 28 08 02 ff f0'), 331 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 332 | bytes.fromhex('ff fa 28 03 04 ff f0') 333 | ] 334 | 335 | self.socket_mock.recv = Mock(side_effect=responses) 336 | 337 | # Act 338 | self.telnet.open('mainframe', 23) 339 | 340 | # Assert 341 | self.assertTrue(self.telnet.is_tn3270_negotiated) 342 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 343 | 344 | self.assertSetEqual(self.telnet.tn3270e_functions, set([])) 345 | 346 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 ff f0')) 347 | 348 | def test_tn3270e_function_negotiation_equal(self): 349 | # Arrange 350 | self.telnet = Telnet('IBM-3279-2-E', tn3270e_functions=[TN3270EFunction.BIND_IMAGE, TN3270EFunction.RESPONSES, TN3270EFunction.SYSREQ]) 351 | 352 | responses = [ 353 | bytes.fromhex('ff fd 28'), 354 | bytes.fromhex('ff fa 28 08 02 ff f0'), 355 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 356 | bytes.fromhex('ff fa 28 03 04 00 02 04 ff f0') 357 | ] 358 | 359 | self.socket_mock.recv = Mock(side_effect=responses) 360 | 361 | # Act 362 | self.telnet.open('mainframe', 23) 363 | 364 | # Assert 365 | self.assertTrue(self.telnet.is_tn3270_negotiated) 366 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 367 | 368 | self.assertSetEqual(self.telnet.tn3270e_functions, set([TN3270EFunction.BIND_IMAGE, TN3270EFunction.RESPONSES, TN3270EFunction.SYSREQ])) 369 | 370 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 00 02 04 ff f0')) 371 | 372 | def test_tn3270e_function_negotiation_subset(self): 373 | # Arrange 374 | self.telnet = Telnet('IBM-3279-2-E', tn3270e_functions=[TN3270EFunction.BIND_IMAGE, TN3270EFunction.RESPONSES, TN3270EFunction.SYSREQ]) 375 | 376 | responses = [ 377 | bytes.fromhex('ff fd 28'), 378 | bytes.fromhex('ff fa 28 08 02 ff f0'), 379 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 380 | bytes.fromhex('ff fa 28 03 04 02 ff f0') 381 | ] 382 | 383 | self.socket_mock.recv = Mock(side_effect=responses) 384 | 385 | # Act 386 | self.telnet.open('mainframe', 23) 387 | 388 | # Assert 389 | self.assertTrue(self.telnet.is_tn3270_negotiated) 390 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 391 | 392 | self.assertSetEqual(self.telnet.tn3270e_functions, set([TN3270EFunction.RESPONSES])) 393 | 394 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 00 02 04 ff f0')) 395 | 396 | def test_tn3270e_function_negotiation_common(self): 397 | # Arrange 398 | self.telnet = Telnet('IBM-3279-2-E', tn3270e_functions=[TN3270EFunction.RESPONSES]) 399 | 400 | responses = [ 401 | bytes.fromhex('ff fd 28'), 402 | bytes.fromhex('ff fa 28 08 02 ff f0'), 403 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 404 | bytes.fromhex('ff fa 28 03 07 00 02 04 ff f0'), 405 | bytes.fromhex('ff fa 28 03 04 02 ff f0') 406 | ] 407 | 408 | self.socket_mock.recv = Mock(side_effect=responses) 409 | 410 | # Act 411 | self.telnet.open('mainframe', 23) 412 | 413 | # Assert 414 | self.assertTrue(self.telnet.is_tn3270_negotiated) 415 | self.assertTrue(self.telnet.is_tn3270e_negotiated) 416 | 417 | self.assertSetEqual(self.telnet.tn3270e_functions, set([TN3270EFunction.RESPONSES])) 418 | 419 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fa 28 03 07 02 ff f0')) 420 | 421 | def test_tn3270e_function_negotiation_invalid(self): 422 | # Arrange 423 | self.telnet = Telnet('IBM-3279-2-E', tn3270e_functions=[TN3270EFunction.RESPONSES]) 424 | 425 | responses = [ 426 | bytes.fromhex('ff fd 28'), 427 | bytes.fromhex('ff fa 28 08 02 ff f0'), 428 | bytes.fromhex('ff fa 28 02 04 49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 54 43 50 30 30 30 33 34 ff f0'), 429 | bytes.fromhex('ff fa 28 03 04 00 02 04 ff f0'), 430 | bytes.fromhex('ff fd 18'), 431 | bytes.fromhex('ff fa 18 01 ff f0'), 432 | bytes.fromhex('ff fd 19'), 433 | bytes.fromhex('ff fb 19'), 434 | bytes.fromhex('ff fd 00'), 435 | bytes.fromhex('ff fb 00') 436 | ] 437 | 438 | self.socket_mock.recv = Mock(side_effect=responses) 439 | 440 | # Act 441 | self.telnet.open('mainframe', 23) 442 | 443 | # Assert 444 | self.assertTrue(self.telnet.is_tn3270_negotiated) 445 | self.assertFalse(self.telnet.is_tn3270e_negotiated) 446 | 447 | self.assertSetEqual(self.telnet.tn3270e_functions, set([])) 448 | 449 | self.socket_mock.sendall.assert_any_call(bytes.fromhex('ff fc 28')) 450 | 451 | class ReadMultipleTestCase(unittest.TestCase): 452 | def setUp(self): 453 | self.telnet = Telnet('IBM-3279-2-E') 454 | 455 | self.telnet.socket = create_autospec(socket, instance=True) 456 | 457 | self.telnet.socket_selector = create_autospec(BaseSelector, instance=True) 458 | 459 | self.is_tn3270e_negotiated = False 460 | 461 | selector_key = Mock(fileobj=self.telnet.socket) 462 | 463 | self.telnet.socket_selector.select.return_value = [(selector_key, selectors.EVENT_READ)] 464 | 465 | def test_multiple_records_in_single_recv(self): 466 | # Arrange 467 | self.telnet.socket.recv = Mock(return_value=bytes.fromhex('01 02 03 ff ef 04 05 06 ff ef')) 468 | 469 | # Act and assert 470 | self.assertEqual(self.telnet.read_multiple(), [(bytes.fromhex('01 02 03'), None), (bytes.fromhex('04 05 06'), None)]) 471 | 472 | def test_single_record_spans_multiple_recv(self): 473 | # Arrange 474 | self.telnet.socket.recv = Mock(side_effect=[bytes.fromhex('01 02 03'), bytes.fromhex('04 05 06 ff ef')]) 475 | 476 | # Act and assert 477 | self.assertEqual(self.telnet.read_multiple(), [(bytes.fromhex('01 02 03 04 05 06'), None)]) 478 | 479 | def test_limit(self): 480 | # Arrange 481 | self.telnet.socket.recv = Mock(return_value=bytes.fromhex('01 02 03 ff ef 04 05 06 ff ef')) 482 | 483 | # Act and assert 484 | self.assertEqual(self.telnet.read_multiple(limit=1), [(bytes.fromhex('01 02 03'), None)]) 485 | 486 | def test_timeout(self): 487 | # Arrange 488 | self.telnet.socket.recv = Mock(side_effect=[bytes.fromhex('01 02 03')]) 489 | 490 | selector_key = Mock(fileobj=self.telnet.socket) 491 | 492 | self.telnet.socket_selector.select.side_effect = [[(selector_key, selectors.EVENT_READ)], []] 493 | 494 | # Act and assert 495 | with patch('time.perf_counter') as perf_counter_mock: 496 | perf_counter_mock.side_effect=[1, 3, 3, 7] 497 | 498 | self.telnet.read_multiple(timeout=5) 499 | 500 | self.assertEqual(self.telnet.socket_selector.select.call_count, 2) 501 | 502 | mock_calls = self.telnet.socket_selector.select.mock_calls 503 | 504 | self.assertEqual(mock_calls[0][1][0], 5) 505 | self.assertEqual(mock_calls[1][1][0], 3) 506 | 507 | def test_recv_eof(self): 508 | # Arrange 509 | self.telnet.socket.recv = Mock(return_value=b'') 510 | 511 | self.assertFalse(self.telnet.eof) 512 | 513 | # Act and assert 514 | with self.assertRaises(EOFError): 515 | self.telnet.read_multiple() 516 | 517 | self.assertTrue(self.telnet.eof) 518 | 519 | def test_tn3270e(self): 520 | # Arrange 521 | self.telnet.is_tn3270e_negotiated = True 522 | 523 | self.telnet.socket.recv = Mock(return_value=bytes.fromhex('00 00 00 00 00 01 02 03 ff ef')) 524 | 525 | # Act and assert 526 | self.assertEqual(self.telnet.read_multiple(), [(bytes.fromhex('01 02 03'), TN3270EMessageHeader(TN3270EDataType.DATA_3270, None, TN3270EResponseFlag.NO, 0))]) 527 | 528 | class WriteTestCase(unittest.TestCase): 529 | def test_basic_tn3270(self): 530 | # Arrange 531 | telnet = Telnet('IBM-3279-2-E') 532 | 533 | telnet.socket = create_autospec(socket, instance=True) 534 | 535 | telnet.is_tn3270e_negotiated = False 536 | 537 | # Act 538 | telnet.write(bytes.fromhex('01 02 03 ff 04 05')) 539 | 540 | # Assert 541 | telnet.socket.sendall.assert_called_with(bytes.fromhex('01 02 03 ff ff 04 05 ff ef')) 542 | 543 | def test_tn3270e(self): 544 | # Arrange 545 | telnet = Telnet('IBM-3279-2-E') 546 | 547 | telnet.socket = create_autospec(socket, instance=True) 548 | 549 | telnet.is_tn3270e_negotiated = True 550 | 551 | # Act 552 | telnet.write(bytes.fromhex('01 02 03 ff 04 05')) 553 | 554 | # Assert 555 | telnet.socket.sendall.assert_called_with(bytes.fromhex('00 00 00 00 00 01 02 03 ff ff 04 05 ff ef')) 556 | 557 | class SendTN3270EPositiveResponse(unittest.TestCase): 558 | def test(self): 559 | # Arrange 560 | telnet = Telnet('IBM-3279-2-E') 561 | 562 | telnet.socket = create_autospec(socket, instance=True) 563 | 564 | telnet.is_tn3270e_negotiated = True 565 | telnet.tn3270e_functions = set([TN3270EFunction.RESPONSES]) 566 | 567 | # Act 568 | telnet.send_tn3270e_positive_response(255) 569 | 570 | # Assert 571 | telnet.socket.sendall.assert_called_with(bytes.fromhex('02 00 00 00 ff ff 00 ff ef')) 572 | 573 | def test_tn3270e_not_negotiated(self): 574 | # Arrange 575 | telnet = Telnet('IBM-3279-2-E') 576 | 577 | telnet.socket = create_autospec(socket, instance=True) 578 | 579 | telnet.is_tn3270e_negotiated = False 580 | 581 | # Act and assert 582 | with self.assertRaisesRegex(Exception, 'TN3270E mode not negotiated'): 583 | telnet.send_tn3270e_positive_response(255) 584 | 585 | def test_tn3270e_not_negotiated(self): 586 | # Arrange 587 | telnet = Telnet('IBM-3279-2-E') 588 | 589 | telnet.socket = create_autospec(socket, instance=True) 590 | 591 | telnet.is_tn3270e_negotiated = True 592 | telnet.tn3270e_functions = set() 593 | 594 | # Act and assert 595 | with self.assertRaisesRegex(Exception, 'TN3270E responses not negotiated'): 596 | telnet.send_tn3270e_positive_response(255) 597 | 598 | class SendTN3270ENegativeResponse(unittest.TestCase): 599 | def test(self): 600 | # Arrange 601 | telnet = Telnet('IBM-3279-2-E') 602 | 603 | telnet.socket = create_autospec(socket, instance=True) 604 | 605 | telnet.is_tn3270e_negotiated = True 606 | telnet.tn3270e_functions = set([TN3270EFunction.RESPONSES]) 607 | 608 | # Act 609 | telnet.send_tn3270e_negative_response(255, 0x00) 610 | 611 | # Assert 612 | telnet.socket.sendall.assert_called_with(bytes.fromhex('02 00 01 00 ff ff 00 ff ef')) 613 | 614 | def test_tn3270e_not_negotiated(self): 615 | # Arrange 616 | telnet = Telnet('IBM-3279-2-E') 617 | 618 | telnet.socket = create_autospec(socket, instance=True) 619 | 620 | telnet.is_tn3270e_negotiated = False 621 | 622 | # Act and assert 623 | with self.assertRaisesRegex(Exception, 'TN3270E mode not negotiated'): 624 | telnet.send_tn3270e_negative_response(255, 0x00) 625 | 626 | def test_tn3270e_not_negotiated(self): 627 | # Arrange 628 | telnet = Telnet('IBM-3279-2-E') 629 | 630 | telnet.socket = create_autospec(socket, instance=True) 631 | 632 | telnet.is_tn3270e_negotiated = True 633 | telnet.tn3270e_functions = set() 634 | 635 | # Act and assert 636 | with self.assertRaisesRegex(Exception, 'TN3270E responses not negotiated'): 637 | telnet.send_tn3270e_negative_response(255, 0x00) 638 | 639 | class EncodeRFC1646TerminalTypeTestCase(unittest.TestCase): 640 | def test_no_device_name(self): 641 | self.assertEqual(encode_rfc1646_terminal_type('IBM-3279-2-E', None), bytes.fromhex('49 42 4d 2d 33 32 37 39 2d 32 2d 45')) 642 | 643 | def test_device_name(self): 644 | self.assertEqual(encode_rfc1646_terminal_type('IBM-3279-2-E', 'LU1'), bytes.fromhex('49 42 4d 2d 33 32 37 39 2d 32 2d 45 40 4c 55 31')) 645 | 646 | class EncodeRFC2355DeviceTypeTestCase(unittest.TestCase): 647 | def test_no_device_name(self): 648 | self.assertEqual(encode_rfc2355_device_type('IBM-3278-2-E', None), bytes.fromhex('49 42 4d 2d 33 32 37 38 2d 32 2d 45')) 649 | 650 | def test_device_name(self): 651 | self.assertEqual(encode_rfc2355_device_type('IBM-3278-2-E', 'LU1'), bytes.fromhex('49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 4c 55 31')) 652 | 653 | class DecodeRFC2355DeviceTypeTestCase(unittest.TestCase): 654 | def test_no_device_name(self): 655 | self.assertEqual(decode_rfc2355_device_type(bytes.fromhex('49 42 4d 2d 33 32 37 38 2d 32 2d 45')), ('IBM-3278-2-E', None)) 656 | 657 | def test_device_name(self): 658 | self.assertEqual(decode_rfc2355_device_type(bytes.fromhex('49 42 4d 2d 33 32 37 38 2d 32 2d 45 01 4c 55 31')), ('IBM-3278-2-E', 'LU1')) 659 | -------------------------------------------------------------------------------- /tn3270/__about__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.15.2' 2 | -------------------------------------------------------------------------------- /tn3270/__init__.py: -------------------------------------------------------------------------------- 1 | from .__about__ import __version__ 2 | 3 | from .telnet import ( 4 | Telnet, 5 | TN3270EFunction, 6 | TN3270EDataType, 7 | TN3270ERequestFlag, 8 | TN3270EResponseFlag 9 | ) 10 | 11 | from .datastream import AID 12 | 13 | from .attributes import ( 14 | Highlight, 15 | Color 16 | ) 17 | 18 | from .emulator import ( 19 | Emulator, 20 | AttributeCell, 21 | CharacterCell, 22 | CharacterSet, 23 | OperatorError, 24 | ProtectedCellOperatorError, 25 | FieldOverflowOperatorError 26 | ) 27 | -------------------------------------------------------------------------------- /tn3270/attributes.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.attributes 3 | ~~~~~~~~~~~~~~~~~ 4 | """ 5 | 6 | from enum import IntEnum 7 | 8 | class Attribute: 9 | """Attribute.""" 10 | 11 | def __init__(self, value): 12 | # TODO: Validate input - looks like there is a parity bit. 13 | self._original_value = value 14 | 15 | self.protected = bool(value & 0x20) 16 | self.numeric = bool(value & 0x10) 17 | self.skip = self.protected and self.numeric 18 | 19 | display = (value & 0x0c) >> 2 20 | 21 | self.intensified = (display == 2) 22 | self.hidden = (display == 3) 23 | 24 | self.modified = bool(value & 0x01) 25 | 26 | @property 27 | def value(self): 28 | # TODO: Reconstruct the entire attribute from parts, this assumes 29 | # modified is the only part that can change (which is true). 30 | return (self._original_value & 0xfe) | int(self.modified) 31 | 32 | def __repr__(self): 33 | return (f'') 36 | 37 | class ExtendedAttributeType(IntEnum): 38 | """Extended attribute type.""" 39 | 40 | ALL = 0x00 41 | HIGHLIGHT = 0x41 42 | FOREGROUND_COLOR = 0x42 43 | 44 | class ExtendedAttribute: 45 | """Extended attribute.""" 46 | 47 | def __init__(self, type_, value): 48 | self.type_ = type_ 49 | self.value = value 50 | 51 | def __repr__(self): 52 | return f'' 53 | 54 | class AllExtendedAttribute(ExtendedAttribute): 55 | """Reset all character attributes extended attribute.""" 56 | 57 | def __init__(self, type_, value): 58 | super().__init__(type_, value) 59 | 60 | class Highlight(IntEnum): 61 | """Highlight.""" 62 | 63 | NORMAL = 0xf0 64 | BLINK = 0xf1 65 | REVERSE = 0xf2 66 | UNDERSCORE = 0xf4 67 | INTENSIFY = 0xf8 68 | 69 | class HighlightExtendedAttribute(ExtendedAttribute): 70 | """Highlight extended attribute.""" 71 | 72 | def __init__(self, type_, value): 73 | super().__init__(type_, value) 74 | 75 | self.blink = False 76 | self.reverse = False 77 | self.underscore = False 78 | 79 | if value == Highlight.BLINK: 80 | self.blink = True 81 | elif value == Highlight.REVERSE: 82 | self.reverse = True 83 | elif value == Highlight.UNDERSCORE: 84 | self.underscore = True 85 | 86 | def __repr__(self): 87 | return (f'') 89 | 90 | class Color(IntEnum): 91 | """Color.""" 92 | 93 | NEUTRAL = 0x00 94 | BLUE = 0xf1 95 | RED = 0xf2 96 | PINK = 0xf3 97 | GREEN = 0xf4 98 | TURQUOISE = 0xf5 99 | YELLOW = 0xf6 100 | WHITE = 0xf7 101 | 102 | class ForegroundColorExtendedAttribute(ExtendedAttribute): 103 | """Foreground extended attribute.""" 104 | 105 | def __init__(self, type_, value): 106 | super().__init__(type_, value) 107 | 108 | self.color = value 109 | 110 | def __repr__(self): 111 | return f'' 112 | -------------------------------------------------------------------------------- /tn3270/datastream.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.datastream 3 | ~~~~~~~~~~~~~~~~~ 4 | """ 5 | 6 | from enum import Enum 7 | import logging 8 | 9 | from .attributes import Attribute, ExtendedAttributeType, ExtendedAttribute, \ 10 | AllExtendedAttribute, HighlightExtendedAttribute, \ 11 | ForegroundColorExtendedAttribute 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class Command(Enum): 16 | """Command.""" 17 | 18 | W = 0x01 # Write 19 | RB = 0x02 # Read Buffer 20 | NOP = 0x03 21 | EW = 0x05 # Erase / Write 22 | RM = 0x06 # Read Modified 23 | EWA = 0x0d # Erase / Write Alternate 24 | RMA = 0x0e # Read Modified All 25 | EAU = 0x0f # Erase All Unprotected 26 | WSF = 0x11 # Write Structured Field 27 | 28 | COMMAND_MAP = { 29 | **{command.value: command for command in Command}, 30 | 31 | # SNA 32 | 0x63: Command.RMA, 33 | 0x6f: Command.EAU, 34 | 0x7e: Command.EWA, 35 | 0xf1: Command.W, 36 | 0xf2: Command.RB, 37 | 0xf3: Command.WSF, 38 | 0xf5: Command.EW, 39 | 0xf6: Command.RM 40 | } 41 | 42 | class WCC: 43 | """Write control character.""" 44 | 45 | def __init__(self, value): 46 | # TODO: Validate input. 47 | self.value = value 48 | 49 | self.reset = bool(value & 0x40) 50 | self.alarm = bool(value & 0x04) 51 | self.unlock_keyboard = bool(value & 0x02) 52 | self.reset_modified = bool(value & 0x01) 53 | 54 | def __repr__(self): 55 | return (f'') 58 | 59 | class Order(Enum): 60 | """Order.""" 61 | 62 | PT = 0x05 # Program Tab 63 | GE = 0x08 # Graphic Escape 64 | SBA = 0x11 # Set Buffer Address 65 | EUA = 0x12 # Erase Unprotected to Address 66 | IC = 0x13 # Insert Cursor 67 | SF = 0x1d # Start Field 68 | SA = 0x28 # Set Attribute 69 | SFE = 0x29 # Start Field Extended 70 | MF = 0x2c # Modify Field 71 | RA = 0x3c # Repeat to Address 72 | 73 | ORDERS = {order.value for order in Order} 74 | 75 | class AID(Enum): 76 | """Attention identifier.""" 77 | 78 | NONE = 0x60 79 | STRUCTURED_FIELD = 0x88 80 | CLEAR = 0x6d 81 | ENTER = 0x7d 82 | PA1 = 0x6c 83 | PA2 = 0x6e 84 | PA3 = 0x6b 85 | PF1 = 0xf1 86 | PF2 = 0xf2 87 | PF3 = 0xf3 88 | PF4 = 0xf4 89 | PF5 = 0xf5 90 | PF6 = 0xf6 91 | PF7 = 0xf7 92 | PF8 = 0xf8 93 | PF9 = 0xf9 94 | PF10 = 0x7a 95 | PF11 = 0x7b 96 | PF12 = 0x7c 97 | PF13 = 0xc1 98 | PF14 = 0xc2 99 | PF15 = 0xc3 100 | PF16 = 0xc4 101 | PF17 = 0xc5 102 | PF18 = 0xc6 103 | PF19 = 0xc7 104 | PF20 = 0xc8 105 | PF21 = 0xc9 106 | PF22 = 0x4a 107 | PF23 = 0x4b 108 | PF24 = 0x4c 109 | 110 | SHORT_READ_AIDS = [AID.CLEAR, AID.PA1, AID.PA2, AID.PA3] 111 | 112 | def parse_outbound_message(bytes_): 113 | """Parse a message from the host.""" 114 | command_byte = bytes_[0] 115 | 116 | command = COMMAND_MAP.get(command_byte) 117 | 118 | if command is None: 119 | raise ValueError(f'Unrecognized command 0x{command_byte:02x}') 120 | 121 | if command in [Command.W, Command.EW, Command.EWA]: 122 | # TODO: Validate size. 123 | 124 | wcc = WCC(bytes_[1]) 125 | orders = list(parse_orders(bytes_[2:])) 126 | 127 | return (command, wcc, orders) 128 | 129 | if command == Command.WSF: 130 | structured_fields = list(parse_outbound_structured_fields(bytes_[1:])) 131 | 132 | return (command, structured_fields) 133 | 134 | return (command,) 135 | 136 | def format_inbound_read_buffer_message(aid, cursor_address, orders): 137 | """Format a read buffer message for the host.""" 138 | bytes_ = bytearray() 139 | 140 | for (order, data) in orders: 141 | if order == Order.SF: 142 | bytes_.extend([Order.SF.value, data[0].value]) 143 | elif order == Order.GE: 144 | bytes_.extend([Order.GE.value, data[0]]) 145 | elif order is None: 146 | bytes_ += data 147 | else: 148 | raise NotImplementedError(f'{order} is not supported') 149 | 150 | return _format_inbound_message(aid, cursor_address, bytes_) 151 | 152 | def format_inbound_read_modified_message(aid, cursor_address, orders, all_=False): 153 | """Format a read modified message for the host.""" 154 | if aid in SHORT_READ_AIDS and not all_: 155 | return bytearray([aid.value]) 156 | 157 | bytes_ = bytearray() 158 | 159 | for (order, data) in orders: 160 | if order == Order.SBA: 161 | bytes_.append(Order.SBA.value) 162 | bytes_.extend(format_address(data[0])) 163 | elif order == Order.SF: 164 | bytes_.extend([Order.SF.value, data[0].value]) 165 | elif order == Order.GE: 166 | bytes_.extend([Order.GE.value, data[0]]) 167 | elif order is None: 168 | bytes_ += data 169 | else: 170 | raise NotImplementedError(f'{order} is not supported') 171 | 172 | return _format_inbound_message(aid, cursor_address, bytes_) 173 | 174 | def parse_orders(bytes_): 175 | """Parse orders from the host.""" 176 | data = bytearray() 177 | 178 | index = 0 179 | 180 | while index < len(bytes_): 181 | byte = bytes_[index] 182 | 183 | if byte in ORDERS: 184 | if data: 185 | yield (None, data) 186 | 187 | data = bytearray() 188 | 189 | order = Order(byte) 190 | parameters = None 191 | 192 | index += 1 193 | 194 | if order == Order.PT: 195 | pass 196 | elif order == Order.GE: 197 | # TODO: validate size 198 | parameters = [bytes_[index]] 199 | index += 1 200 | elif order == Order.SBA: 201 | # TODO: validate size 202 | parameters = [parse_address(bytes_[index:index+2])[0]] 203 | index += 2 204 | elif order == Order.EUA: 205 | # TODO: validate size 206 | parameters = [parse_address(bytes_[index:index+2])[0]] 207 | index += 2 208 | elif order == Order.IC: 209 | pass 210 | elif order == Order.SF: 211 | parameters = [Attribute(bytes_[index])] 212 | index += 1 213 | elif order == Order.SA: 214 | # TODO: validate size 215 | parameters = [parse_extended_attribute(bytes_[index:index+2])] 216 | index += 2 217 | elif order in [Order.SFE, Order.MF]: 218 | # TODO: validate size 219 | attribute = None 220 | extended_attributes = [] 221 | 222 | count = bytes_[index] 223 | index += 1 224 | 225 | for attribute_index in range(index, index + (count * 2), 2): 226 | if bytes_[attribute_index] == 0xc0: 227 | attribute = Attribute(bytes_[attribute_index+1]) 228 | else: 229 | extended_attributes.append(parse_extended_attribute(bytes_[attribute_index:attribute_index+2])) 230 | 231 | parameters = [attribute, extended_attributes] 232 | index += count * 2 233 | elif order == Order.RA: 234 | # TODO: validate size 235 | stop_address = parse_address(bytes_[index:index+2])[0] 236 | index += 2 237 | 238 | # Peek ahead to detect a GE order. 239 | is_ge = False 240 | 241 | if bytes_[index] == Order.GE.value: 242 | is_ge = True 243 | index += 1 244 | 245 | parameters = [stop_address, bytes_[index], is_ge] 246 | index += 1 247 | 248 | yield (order, parameters) 249 | else: 250 | if byte == 0x00 or (byte >= 0x40 and byte <= 0xfe): 251 | data.append(byte) 252 | else: 253 | logger.warning(f'Value 0x{byte:02x} out of range') 254 | 255 | index += 1 256 | 257 | if data: 258 | yield (None, data) 259 | 260 | def parse_outbound_structured_fields(bytes_): 261 | """Parse structured fields from the host.""" 262 | index = 0 263 | 264 | while index < len(bytes_): 265 | remaining_length = len(bytes_) - index 266 | 267 | if remaining_length < 2: 268 | raise Exception('Invalid structured field') 269 | 270 | length = (bytes_[index] << 8) | bytes_[index+1] 271 | 272 | if length == 0: 273 | length = remaining_length 274 | 275 | if length < 3: 276 | raise Exception(f'Invalid structured field length: {length} must be at least 3') 277 | 278 | if length > remaining_length: 279 | raise Exception(f'Invalid structured field length: {length} greater than remaining {remaining_length} bytes') 280 | 281 | id_ = bytes_[index+2] 282 | 283 | data_length = length - 3 284 | data = bytes_[index+3:index+3+data_length] 285 | 286 | yield (id_, data) 287 | 288 | index += length 289 | 290 | def format_inbound_structured_fields(structured_fields): 291 | """Format structured fields for the host.""" 292 | bytes_ = bytearray([AID.STRUCTURED_FIELD.value]) 293 | 294 | for (id_, data) in structured_fields: 295 | length = len(data) + 3 296 | 297 | bytes_.extend([(length >> 8) & 0xff, length & 0xff, id_]) 298 | 299 | bytes_ += data 300 | 301 | return bytes_ 302 | 303 | def parse_extended_attribute(bytes_): 304 | """Parse an extended attribute.""" 305 | 306 | if len(bytes_) != 2: 307 | raise Exception('Invalid extended attribute') 308 | 309 | type_ = bytes_[0] 310 | value = bytes_[1] 311 | 312 | if type_ == ExtendedAttributeType.ALL: 313 | # TODO: value should be 0x00 314 | # TODO: This should only be valid for SA orders... 315 | return AllExtendedAttribute(type_, value) 316 | elif type_ == ExtendedAttributeType.HIGHLIGHT: 317 | return HighlightExtendedAttribute(type_, value) 318 | elif type_ == ExtendedAttributeType.FOREGROUND_COLOR: 319 | return ForegroundColorExtendedAttribute(type_, value) 320 | 321 | logger.warning(f'Extended attribute 0x{type_:02x} not supported') 322 | 323 | return ExtendedAttribute(type_, value) 324 | 325 | def parse_address(bytes_, size=None): 326 | """Parse an address.""" 327 | if size == 16: 328 | return((bytes_[0] << 8) | bytes_[1], 16) 329 | 330 | setting = (bytes_[0] & 0xc0) >> 6 331 | 332 | # Handle a 12-bit address. 333 | if setting in [0b01, 0b11]: 334 | return (((bytes_[0] & 0x3f) << 6) | (bytes_[1] & 0x3f), 12) 335 | 336 | # Assume a 14-bit address. 337 | return (((bytes_[0] & 0x3f) << 8) | bytes_[1], 14) 338 | 339 | # https://www.tommysprinkle.com/mvs/P3270/iocodes.htm 340 | SIX_BIT_CHARACTER_MAP = [ 341 | 0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 342 | 0xc8, 0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 343 | 0x50, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 344 | 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 345 | 0x60, 0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 346 | 0xe8, 0xe9, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 347 | 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 348 | 0xf8, 0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f 349 | ] 350 | 351 | def format_address(address, size=12): 352 | """Format an address.""" 353 | 354 | # TODO: Validate that the address is within range based on size. 355 | 356 | if size == 16: 357 | return bytes([(address >> 8) & 0xff, address & 0xff]) 358 | 359 | if size == 14: 360 | return bytes([(address >> 8) & 0x3f, address & 0xff]) 361 | 362 | if size == 12: 363 | return bytes([SIX_BIT_CHARACTER_MAP[(address >> 6) & 0x3f], 364 | SIX_BIT_CHARACTER_MAP[address & 0x3f]]) 365 | 366 | raise ValueError('Invalid size') 367 | 368 | def _format_inbound_message(aid, cursor_address, data_bytes): 369 | message_bytes = bytearray([aid.value]) 370 | 371 | message_bytes += format_address(cursor_address) 372 | message_bytes += data_bytes 373 | 374 | return message_bytes 375 | -------------------------------------------------------------------------------- /tn3270/ebcdic.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.ebcdic 3 | ~~~~~~~~~~~~~ 4 | 5 | EBCDIC constants 6 | """ 7 | 8 | DUP = 0x1c 9 | FM = 0x1e 10 | -------------------------------------------------------------------------------- /tn3270/emulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.emulator 3 | ~~~~~~~~~~~~~~~ 4 | """ 5 | 6 | from enum import Enum 7 | from itertools import chain 8 | import struct 9 | import logging 10 | 11 | from .datastream import Command, WCC, Order, AID, parse_outbound_message, \ 12 | format_inbound_read_buffer_message, \ 13 | format_inbound_read_modified_message, \ 14 | parse_orders, format_inbound_structured_fields 15 | from .attributes import Attribute, AllExtendedAttribute, \ 16 | Highlight, HighlightExtendedAttribute, \ 17 | Color, ForegroundColorExtendedAttribute 18 | from .structured_fields import StructuredField, ReadPartitionType, \ 19 | QueryListRequestType, QueryCode 20 | from .ebcdic import DUP, FM 21 | from .telnet import TN3270EDataType, TN3270EResponseFlag 22 | 23 | class CharacterSet(Enum): 24 | """Display cell character set.""" 25 | 26 | GE = 1 27 | 28 | class CellFormatting: 29 | """Display cell formatting.""" 30 | 31 | def __init__(self, formatting=None, extended_attributes=None): 32 | self.blink = False 33 | self.reverse = False 34 | self.underscore = False 35 | self.color = 0x00 36 | 37 | if formatting is not None: 38 | self.blink = formatting.blink 39 | self.reverse = formatting.reverse 40 | self.underscore = formatting.underscore 41 | self.color = formatting.color 42 | 43 | if extended_attributes is not None: 44 | for extended_attribute in extended_attributes: 45 | self._apply_extended_attribute(extended_attribute) 46 | 47 | def _apply_extended_attribute(self, extended_attribute): 48 | if isinstance(extended_attribute, AllExtendedAttribute): 49 | self.blink = False 50 | self.reverse = False 51 | self.underscore = False 52 | self.color = 0x00 53 | elif isinstance(extended_attribute, HighlightExtendedAttribute): 54 | self.blink = extended_attribute.blink 55 | self.reverse = extended_attribute.reverse 56 | self.underscore = extended_attribute.underscore 57 | elif isinstance(extended_attribute, ForegroundColorExtendedAttribute): 58 | self.color = extended_attribute.color 59 | 60 | def __eq__(self, other): 61 | if not isinstance(other, CellFormatting): 62 | return False 63 | 64 | return other.blink == self.blink and other.reverse == self.reverse and \ 65 | other.underscore == self.underscore and other.color == self.color 66 | 67 | class Cell: 68 | """A display cell.""" 69 | 70 | def __init__(self, formatting): 71 | self.formatting = formatting 72 | 73 | class AttributeCell(Cell): 74 | """A attribute display cell.""" 75 | 76 | def __init__(self, attribute, formatting=None): 77 | super().__init__(formatting) 78 | 79 | self.attribute = attribute 80 | 81 | class CharacterCell(Cell): 82 | """A character display cell.""" 83 | 84 | def __init__(self, byte, character_set=None, formatting=None): 85 | super().__init__(formatting) 86 | 87 | self.byte = byte 88 | self.character_set = character_set 89 | 90 | class OperatorError(Exception): 91 | """Operator error.""" 92 | 93 | class ProtectedCellOperatorError(OperatorError): 94 | """Protected cell error.""" 95 | 96 | class FieldOverflowOperatorError(OperatorError): 97 | """Field overflow error.""" 98 | 99 | class Emulator: 100 | """TN3270 emulator.""" 101 | 102 | def __init__(self, stream, rows, columns, supported_colors=8, 103 | supported_highlights=[Highlight.BLINK, Highlight.REVERSE, 104 | Highlight.UNDERSCORE]): 105 | self.logger = logging.getLogger(__name__) 106 | 107 | # TODO: Validate that stream has read_multiple() and write() methods. 108 | self.stream = stream 109 | 110 | if rows < 24: 111 | raise ValueError('Invalid rows, must be at least 24') 112 | 113 | if columns < 80: 114 | raise ValueError('Invalid columns, must be at least 80') 115 | 116 | if supported_colors not in [1, 4, 8]: 117 | raise ValueError('Invalid suported colors, must be 1, 4 or 8') 118 | 119 | self.default_dimensions = (24, 80) 120 | self.alternate_dimensions = (rows, columns) 121 | 122 | self.supported_colors = supported_colors 123 | self.supported_highlights = supported_highlights 124 | 125 | (self.rows, self.columns) = self.default_dimensions 126 | self.alternate = False 127 | 128 | self.cells = [CharacterCell(0x00) for index in range(rows * columns)] 129 | self.dirty = set(range(rows * columns)) 130 | 131 | self.address = 0 132 | self.cursor_address = 0 133 | 134 | self.current_aid = AID.NONE 135 | self.keyboard_locked = True 136 | 137 | def update(self, **kwargs): 138 | """Read and execute outbound messages.""" 139 | self.logger.debug('Update') 140 | 141 | records = self.stream.read_multiple(**kwargs) 142 | 143 | if not records: 144 | return False 145 | 146 | for (bytes_, tn3270e) in records: 147 | if self.logger.isEnabledFor(logging.DEBUG): 148 | self.logger.debug(f'\tRecord = {bytes_}') 149 | self.logger.debug(f'\tTN3270E = {tn3270e}') 150 | 151 | if tn3270e and tn3270e.data_type != TN3270EDataType.DATA_3270: 152 | self.logger.warning(f'Unsupported TN3270E DATA-TYPE {tn3270e.data_type}') 153 | continue 154 | 155 | try: 156 | (command, *options) = parse_outbound_message(bytes_) 157 | 158 | self._execute(command, *options) 159 | except: 160 | if tn3270e and tn3270e.response_flag in [TN3270EResponseFlag.ERROR, TN3270EResponseFlag.ALWAYS]: 161 | # TODO: Distinguish between invalid command ("command reject") and invalid 162 | # address or order sequence ("operation check"). 163 | self.stream.send_tn3270e_negative_response(tn3270e.sequence_number, 0x00) 164 | 165 | raise 166 | 167 | if tn3270e and tn3270e.response_flag == TN3270EResponseFlag.ALWAYS: 168 | self.stream.send_tn3270e_positive_response(tn3270e.sequence_number) 169 | 170 | return True 171 | 172 | def aid(self, aid): 173 | """AID key.""" 174 | if aid == AID.CLEAR: 175 | self._clear() 176 | 177 | self.current_aid = aid 178 | self.keyboard_locked = True 179 | 180 | self._read_modified() 181 | 182 | def tab(self, direction=1): 183 | """Tab or backtab key.""" 184 | address = self._calculate_tab_address(self.cursor_address, direction) 185 | 186 | if address is not None: 187 | self.cursor_address = address 188 | 189 | def newline(self): 190 | """Move to the next line or the subsequent unprotected field.""" 191 | current_row = self.cursor_address // self.columns 192 | 193 | address = self._wrap_address((current_row + 1) * self.columns) 194 | 195 | (attribute, attribute_address) = self.find_attribute(address) 196 | 197 | if attribute is not None and not attribute.protected and attribute_address != address: 198 | self.cursor_address = address 199 | return 200 | 201 | address = self._calculate_tab_address(address, direction=1) 202 | 203 | if address is not None: 204 | self.cursor_address = address 205 | 206 | def home(self): 207 | """Home key.""" 208 | if not self.is_formatted(): 209 | self.cursor_address = 0 210 | return 211 | 212 | addresses = self._get_addresses(0, (self.rows * self.columns) - 1) 213 | 214 | address = next((address for address in addresses 215 | if isinstance(self.cells[address], AttributeCell) 216 | and not self.cells[address].attribute.protected), None) 217 | 218 | if address is not None: 219 | self.cursor_address = self._wrap_address(address + 1) 220 | 221 | def cursor_up(self): 222 | """Cursor up key.""" 223 | self.cursor_address = self._wrap_address(self.cursor_address - self.columns) 224 | 225 | def cursor_down(self): 226 | """Cursor down key.""" 227 | self.cursor_address = self._wrap_address(self.cursor_address + self.columns) 228 | 229 | def cursor_left(self, rate=1): 230 | """Cursor left key.""" 231 | if rate < 1 or rate > 2: 232 | raise ValueError('Invalid rate') 233 | 234 | self.cursor_address = self._wrap_address(self.cursor_address - rate) 235 | 236 | def cursor_right(self, rate=1): 237 | """Cursor right key.""" 238 | if rate < 1 or rate > 2: 239 | raise ValueError('Invalid rate') 240 | 241 | self.cursor_address = self._wrap_address(self.cursor_address + rate) 242 | 243 | def input(self, byte, insert=False): 244 | """Single character input.""" 245 | self._input(byte, insert=insert) 246 | 247 | def dup(self, insert=False): 248 | """Duplicate (DUP) key.""" 249 | self._input(DUP, insert=insert, move=False) 250 | 251 | # TODO: Moving to the next unprotected field should be reusable - should the 252 | # calculate_tab_address method be refactored to be more generic or at least 253 | # a single next_unprotected filter? 254 | addresses = self._get_addresses(self.cursor_address, 255 | self._wrap_address(self.cursor_address - 1)) 256 | 257 | address = next((address for address in addresses 258 | if isinstance(self.cells[address], AttributeCell) 259 | and not self.cells[address].attribute.protected), None) 260 | 261 | if address is not None: 262 | self.cursor_address = self._wrap_address(address + 1) 263 | 264 | def field_mark(self, insert=False): 265 | """Field mark (FM) key.""" 266 | self._input(FM, insert=insert) 267 | 268 | def backspace(self): 269 | """Backspace key.""" 270 | if isinstance(self.cells[self.cursor_address], AttributeCell): 271 | raise ProtectedCellOperatorError 272 | 273 | (start_address, end_address, attribute) = self.get_unprotected_field(self.cursor_address) 274 | 275 | if self.cursor_address == start_address: 276 | return 277 | 278 | self._shift_left(self._wrap_address(self.cursor_address - 1), end_address) 279 | 280 | if attribute is not None: 281 | attribute.modified = True 282 | 283 | self.cursor_address = self._wrap_address(self.cursor_address - 1) 284 | 285 | def delete(self): 286 | """Delete key.""" 287 | if not self.is_formatted(): 288 | return 289 | 290 | if isinstance(self.cells[self.cursor_address], AttributeCell): 291 | raise ProtectedCellOperatorError 292 | 293 | (_, end_address, attribute) = self.get_unprotected_field(self.cursor_address) 294 | 295 | self._shift_left(self.cursor_address, end_address) 296 | 297 | if attribute is not None: 298 | attribute.modified = True 299 | 300 | def erase_end_of_field(self): 301 | """Erase end of field (EOF) key.""" 302 | if self.is_formatted(): 303 | if isinstance(self.cells[self.cursor_address], AttributeCell): 304 | raise ProtectedCellOperatorError 305 | 306 | (_, end_address, attribute) = self.get_unprotected_field(self.cursor_address) 307 | 308 | for address in self._get_addresses(self.cursor_address, end_address): 309 | self._write_character(address, 0x00, preserve=True) 310 | 311 | attribute.modified = True 312 | else: 313 | for address in self._get_addresses(self.cursor_address, (self.rows * self.columns) - 1): 314 | self._write_character(address, 0x00, preserve=True) 315 | 316 | def erase_input(self): 317 | """Erase input key.""" 318 | if self.is_formatted(): 319 | for (start_address, end_address, attribute) in self.get_fields(protected=False): 320 | for address in self._get_addresses(start_address, end_address): 321 | self._write_character(address, 0x00, preserve=True) 322 | 323 | attribute.modified = False 324 | 325 | self.cursor_address = self._calculate_tab_address(0, 1) or 0 326 | else: 327 | self._clear() 328 | 329 | def get_bytes(self, start_address, end_address): 330 | """Get character cell bytes.""" 331 | addresses = self._get_addresses(start_address, end_address) 332 | 333 | return bytes([self.cells[address].byte if isinstance(self.cells[address], CharacterCell) else 0x00 for address in addresses]) 334 | 335 | def is_formatted(self): 336 | """Is there at least one attribute?""" 337 | return any([isinstance(cell, AttributeCell) for cell in self.cells]) 338 | 339 | def get_field(self, address): 340 | """Get the field containing or starting at the address.""" 341 | (attribute, start_attribute_address) = self.find_attribute(address) 342 | 343 | start_address = self._wrap_address(start_attribute_address + 1) 344 | 345 | # By using the field start attribute address as the search end address we know 346 | # there will be at least one attribute byte found even in the case of a single 347 | # field. 348 | addresses = self._get_addresses(start_address, start_attribute_address) 349 | 350 | end_attribute_address = next((address for address in addresses 351 | if isinstance(self.cells[address], AttributeCell))) 352 | 353 | end_address = self._wrap_address(end_attribute_address - 1) 354 | 355 | return (start_address, end_address, attribute) 356 | 357 | def get_unprotected_field(self, address): 358 | """Get the unprotected field containing or starting at the address.""" 359 | field = self.get_field(address) 360 | 361 | attribute = field[2] 362 | 363 | if attribute is None or attribute.protected: 364 | raise ProtectedCellOperatorError 365 | 366 | return field 367 | 368 | def get_fields(self, protected=None, modified=None): 369 | """Get fields.""" 370 | fields = [] 371 | 372 | for address in range(0, self.rows * self.columns): 373 | cell = self.cells[address] 374 | 375 | if isinstance(cell, AttributeCell): 376 | attribute = cell.attribute 377 | 378 | if protected is not None and attribute.protected != protected: 379 | continue 380 | 381 | if modified is not None and attribute.modified != modified: 382 | continue 383 | 384 | field = self.get_field(address) 385 | 386 | fields.append(field) 387 | 388 | return fields 389 | 390 | def find_attribute(self, address): 391 | """Find the applicable attribute for the address.""" 392 | addresses = self._get_addresses(address, self._wrap_address(address + 1), 393 | direction=-1) 394 | 395 | for address in addresses: 396 | cell = self.cells[address] 397 | 398 | if isinstance(cell, AttributeCell): 399 | return (cell.attribute, address) 400 | 401 | return (None, None) 402 | 403 | def alarm(self): 404 | """Alarm stub.""" 405 | pass 406 | 407 | def _execute(self, command, *options): 408 | if self.logger.isEnabledFor(logging.DEBUG): 409 | self.logger.debug('Execute') 410 | self.logger.debug(f'\tCommand = {command}') 411 | 412 | if command == Command.W: 413 | self._write(*options) 414 | elif command == Command.RB: 415 | self._read_buffer() 416 | elif command == Command.NOP: 417 | pass 418 | elif command in [Command.EW, Command.EWA]: 419 | self._erase(command == Command.EWA) 420 | self._write(*options) 421 | elif command == Command.RM: 422 | self._read_modified() 423 | elif command == Command.RMA: 424 | self._read_modified(all_=True) 425 | elif command == Command.EAU: 426 | self._erase_all_unprotected() 427 | elif command == Command.WSF: 428 | self._write_structured_fields(*options) 429 | 430 | def _erase(self, alternate): 431 | self.logger.debug('Erase') 432 | self.logger.debug(f'\tAlternate = {alternate}') 433 | 434 | self._clear() 435 | 436 | if alternate: 437 | (self.rows, self.columns) = self.alternate_dimensions 438 | else: 439 | (self.rows, self.columns) = self.default_dimensions 440 | 441 | self.alternate = alternate 442 | 443 | def _erase_all_unprotected(self): 444 | self.logger.debug('Erase All Unprotected') 445 | 446 | if self.is_formatted(): 447 | for (start_address, end_address, attribute) in self.get_fields(protected=False): 448 | for address in self._get_addresses(start_address, end_address): 449 | self._write_character(address, 0x00, preserve=True) 450 | 451 | attribute.modified = False 452 | 453 | self.cursor_address = self._calculate_tab_address(0, 1) or 0 454 | else: 455 | self._clear() 456 | 457 | self.current_aid = AID.NONE 458 | self.keyboard_locked = False 459 | 460 | def _write(self, wcc, orders): 461 | if self.logger.isEnabledFor(logging.DEBUG): 462 | self.logger.debug('Write') 463 | self.logger.debug(f'\tWCC = {wcc}') 464 | 465 | if wcc.reset_modified: 466 | for cell in self.cells: 467 | if isinstance(cell, AttributeCell): 468 | cell.attribute.modified = False 469 | 470 | field_formatting = None 471 | character_formatting = None 472 | character_set = None 473 | 474 | for (order, data) in orders: 475 | if self.logger.isEnabledFor(logging.DEBUG): 476 | if order is None: 477 | self.logger.debug(f'\t{data}') 478 | else: 479 | self.logger.debug(f'\t{order}') 480 | self.logger.debug(f'\t\tParameters = {data}') 481 | 482 | if order == Order.PT: 483 | # TODO: Implement additional PT cases 484 | if isinstance(self.cells[self.address], AttributeCell) and not self.cells[self.address].attribute.protected: 485 | self.address = self._wrap_address(self.address + 1) 486 | else: 487 | raise NotImplementedError('PT order is not fully supported') 488 | elif order == Order.GE: 489 | self._write_character(self.address, data[0], CharacterSet.GE, None) 490 | 491 | self.address = self._wrap_address(self.address + 1) 492 | elif order == Order.SBA: 493 | if data[0] >= (self.rows * self.columns): 494 | self.logger.warning(f'Address {data[0]} is out of range') 495 | else: 496 | self.address = data[0] 497 | elif order == Order.EUA: 498 | stop_address = data[0] 499 | 500 | # TODO: Validate stop_address is in range... 501 | end_address = self._wrap_address(stop_address - 1) 502 | 503 | addresses = self._get_addresses(self.address, end_address) 504 | unprotected_addresses = self._get_unprotected_addresses() 505 | 506 | for address in unprotected_addresses.intersection(addresses): 507 | self._write_character(address, 0x00, preserve=True) 508 | 509 | self.address = stop_address 510 | elif order == Order.IC: 511 | self.cursor_address = self.address 512 | elif order == Order.SF: 513 | field_formatting = None 514 | 515 | self._write_attribute(self.address, data[0], field_formatting) 516 | 517 | self.address = self._wrap_address(self.address + 1) 518 | elif order == Order.SA: 519 | character_formatting = CellFormatting(character_formatting, extended_attributes=[data[0]]) 520 | elif order == Order.SFE: 521 | # TODO: Confirm that formatting should be reset here 522 | field_formatting = CellFormatting(None, extended_attributes=data[1]) 523 | 524 | self._write_attribute(self.address, data[0] or Attribute(0), field_formatting) 525 | 526 | self.address = self._wrap_address(self.address + 1) 527 | elif order == Order.MF: 528 | if isinstance(self.cells[self.address], AttributeCell): 529 | (attribute, extended_attributes) = data 530 | 531 | if attribute is not None: 532 | self.cells[self.address].attribute = attribute 533 | 534 | if extended_attributes: 535 | existing_formatting = self.cells[self.address].formatting 536 | 537 | # TODO: Confirm that this should affect "global" formatting... 538 | field_formatting = CellFormatting(existing_formatting, extended_attributes) 539 | 540 | self.cells[self.address].formatting = field_formatting 541 | 542 | self.address = self._wrap_address(self.address + 1) 543 | else: 544 | self.logger.warning('MF order rejected as cell is not attribute cell') 545 | elif order == Order.RA: 546 | (stop_address, byte, is_ge) = data 547 | 548 | # TODO: Validate stop_address is in range... 549 | end_address = self._wrap_address(stop_address - 1) 550 | 551 | addresses = self._get_addresses(self.address, end_address) 552 | 553 | for address in addresses: 554 | self._write_character(address, byte, CharacterSet.GE if is_ge else character_set, character_formatting) 555 | 556 | self.address = stop_address 557 | elif order is None: 558 | for byte in data: 559 | self._write_character(self.address, byte, character_set, character_formatting) 560 | 561 | self.address = self._wrap_address(self.address + 1) 562 | 563 | if wcc.unlock_keyboard: 564 | self.current_aid = AID.NONE 565 | self.keyboard_locked = False 566 | 567 | if wcc.alarm: 568 | self.alarm() 569 | 570 | def _clear(self): 571 | for address in range(self.rows * self.columns): 572 | self._write_character(address, 0x00, None, None) 573 | 574 | self.address = 0 575 | self.cursor_address = 0 576 | 577 | def _read_buffer(self): 578 | orders = self._generate_inbound_orders(0, (self.rows * self.columns) - 1) 579 | 580 | if self.logger.isEnabledFor(logging.DEBUG): 581 | self.logger.debug('Read Buffer') 582 | self.logger.debug(f'\tAID = {self.current_aid}') 583 | self.logger.debug(f'\tOrders = {orders}') 584 | 585 | bytes_ = format_inbound_read_buffer_message(self.current_aid, self.cursor_address, orders) 586 | 587 | if self.logger.isEnabledFor(logging.DEBUG): 588 | self.logger.debug(f'\tData = {bytes_}') 589 | 590 | self.stream.write(bytes_) 591 | 592 | def _read_modified(self, all_=False): 593 | if self.is_formatted(): 594 | modified_field_ranges = [(start_address, end_address) for (start_address, end_address, attribute) in self.get_fields(modified=True)] 595 | else: 596 | modified_field_ranges = [(0, (self.rows * self.columns) - 1)] 597 | 598 | orders = [] 599 | 600 | for (start_address, end_address) in modified_field_ranges: 601 | orders.append((Order.SBA, [start_address])) 602 | 603 | orders.extend(self._generate_inbound_orders(start_address, end_address, filter_null=True)) 604 | 605 | if self.logger.isEnabledFor(logging.DEBUG): 606 | self.logger.debug('Read Modified') 607 | self.logger.debug(f'\tAID = {self.current_aid}') 608 | self.logger.debug(f'\tOrders = {orders}') 609 | self.logger.debug(f'\tAll = {all_}') 610 | 611 | bytes_ = format_inbound_read_modified_message(self.current_aid, self.cursor_address, orders, all_) 612 | 613 | if self.logger.isEnabledFor(logging.DEBUG): 614 | self.logger.debug(f'\tData = {bytes_}') 615 | 616 | self.stream.write(bytes_) 617 | 618 | def _input(self, byte, insert=False, move=True): 619 | attribute = None 620 | 621 | if self.is_formatted(): 622 | if isinstance(self.cells[self.cursor_address], AttributeCell): 623 | raise ProtectedCellOperatorError 624 | 625 | (_, end_address, attribute) = self.get_unprotected_field(self.cursor_address) 626 | 627 | # TODO: Implement numeric field validation. 628 | 629 | if insert and self.cells[self.cursor_address].byte != 0x00: 630 | addresses = self._get_addresses(self.cursor_address, end_address) 631 | 632 | first_null_address = next((address for address in addresses 633 | if self.cells[address].byte == 0x00), None) 634 | 635 | if first_null_address is None: 636 | raise FieldOverflowOperatorError 637 | 638 | self._shift_right(self.cursor_address, first_null_address) 639 | else: 640 | if insert: 641 | raise NotImplementedError('Insert input on unformatted screen is not supported') 642 | 643 | self._write_character(self.cursor_address, byte, preserve=True) 644 | 645 | if attribute is not None: 646 | attribute.modified = True 647 | 648 | if not move: 649 | return 650 | 651 | self.cursor_address = self._wrap_address(self.cursor_address + 1) 652 | 653 | # TODO: Is this correct - does this only happen if skip? 654 | if isinstance(self.cells[self.cursor_address], AttributeCell): 655 | skip = self.cells[self.cursor_address].attribute.skip 656 | 657 | addresses = self._get_addresses(self.cursor_address, 658 | self._wrap_address(self.cursor_address - 1)) 659 | 660 | address = next((address for address in addresses 661 | if isinstance(self.cells[address], AttributeCell) 662 | and (not skip or (skip and not self.cells[address].attribute.protected))), None) 663 | 664 | if address is not None: 665 | self.cursor_address = self._wrap_address(address + 1) 666 | 667 | def _get_addresses(self, start_address, end_address, direction=1): 668 | if direction < 0: 669 | if end_address > start_address: 670 | return chain(reversed(range(0, start_address + 1)), 671 | reversed(range(end_address, self.rows * self.columns))) 672 | 673 | return reversed(range(end_address, start_address + 1)) 674 | 675 | if end_address < start_address: 676 | return chain(range(start_address, self.rows * self.columns), 677 | range(0, end_address + 1)) 678 | 679 | return range(start_address, end_address + 1) 680 | 681 | def _wrap_address(self, address): 682 | if address < 0 or address >= (self.rows * self.columns): 683 | return address % (self.rows * self.columns) 684 | 685 | return address 686 | 687 | def _get_unprotected_addresses(self): 688 | addresses = set() 689 | 690 | for (start_address, end_address, _) in self.get_fields(protected=False): 691 | addresses.update(self._get_addresses(start_address, end_address)) 692 | 693 | return addresses 694 | 695 | def _calculate_tab_address(self, address, direction): 696 | if direction < 0: 697 | if address > 0 and isinstance(self.cells[address - 1], AttributeCell): 698 | address -= 1 699 | 700 | start_address = self._wrap_address(address - 1) 701 | end_address = self._wrap_address(address) 702 | else: 703 | start_address = self._wrap_address(address) 704 | end_address = self._wrap_address(address - 1) 705 | 706 | addresses = self._get_addresses(start_address, end_address, direction) 707 | 708 | address = next((address for address in addresses 709 | if isinstance(self.cells[address], AttributeCell) 710 | and not self.cells[address].attribute.protected), None) 711 | 712 | if address is None: 713 | return None 714 | 715 | return self._wrap_address(address + 1) 716 | 717 | def _write_attribute(self, index, attribute, formatting=None, preserve=False): 718 | cell = self.cells[index] 719 | 720 | if preserve: 721 | formatting = cell.formatting 722 | 723 | if isinstance(cell, AttributeCell): 724 | if cell.attribute.value == attribute.value and cell.formatting == formatting: 725 | return False 726 | 727 | cell.attribute = attribute 728 | cell.formatting = formatting 729 | else: 730 | self.cells[index] = AttributeCell(attribute, formatting) 731 | 732 | self.dirty.add(index) 733 | 734 | return True 735 | 736 | def _write_character(self, index, byte, character_set=None, formatting=None, preserve=False): 737 | cell = self.cells[index] 738 | 739 | if preserve: 740 | character_set = cell.character_set 741 | formatting = cell.formatting 742 | 743 | if isinstance(cell, CharacterCell): 744 | if cell.byte == byte and cell.character_set == character_set and cell.formatting == formatting: 745 | return False 746 | 747 | cell.byte = byte 748 | cell.character_set = character_set 749 | cell.formatting = formatting 750 | else: 751 | self.cells[index] = CharacterCell(byte, character_set, formatting) 752 | 753 | self.dirty.add(index) 754 | 755 | return True 756 | 757 | def _shift_left(self, start_address, end_address): 758 | addresses = list(self._get_addresses(start_address, end_address)) 759 | 760 | for (left_address, right_address) in zip(addresses, addresses[1:]): 761 | self._write_character(left_address, self.cells[right_address].byte, preserve=True) 762 | 763 | self._write_character(end_address, 0x00, preserve=True) 764 | 765 | def _shift_right(self, start_address, end_address): 766 | addresses = list(self._get_addresses(start_address, end_address)) 767 | 768 | for (left_address, right_address) in reversed(list(zip(addresses, addresses[1:]))): 769 | self._write_character(right_address, self.cells[left_address].byte, preserve=True) 770 | 771 | self._write_character(start_address, 0x00, preserve=True) 772 | 773 | def _generate_inbound_orders(self, start_address, end_address, filter_null=False): 774 | orders = [] 775 | 776 | data = bytearray() 777 | 778 | def eject(): 779 | nonlocal data 780 | 781 | if data: 782 | orders.append((None, data)) 783 | 784 | data = bytearray() 785 | 786 | for address in self._get_addresses(start_address, end_address): 787 | cell = self.cells[address] 788 | 789 | if isinstance(cell, AttributeCell): 790 | eject() 791 | 792 | orders.append((Order.SF, [cell.attribute])) 793 | elif isinstance(cell, CharacterCell): 794 | if not filter_null or cell.byte != 0x00: 795 | if cell.character_set == CharacterSet.GE: 796 | eject() 797 | 798 | orders.append((Order.GE, [cell.byte])) 799 | else: 800 | data.append(cell.byte) 801 | 802 | eject() 803 | 804 | return orders 805 | 806 | def _write_structured_fields(self, structured_fields): 807 | if self.logger.isEnabledFor(logging.DEBUG): 808 | self.logger.debug('Write Structured Fields') 809 | self.logger.debug(f'\tFields = {structured_fields}') 810 | 811 | for (id_, data) in structured_fields: 812 | if id_ == StructuredField.READ_PARTITION: 813 | self._read_partition(data) 814 | elif id_ == StructuredField.ERASE_RESET: 815 | self._erase(data[0] == 0x80) 816 | elif id_ == StructuredField.OUTBOUND_3270DS: 817 | self._outbound_3270ds(data) 818 | else: 819 | raise NotImplementedError(f'Structured field 0x{id_:02x} not supported') 820 | 821 | def _read_partition(self, data): 822 | if self.logger.isEnabledFor(logging.DEBUG): 823 | self.logger.debug('Read Partition (Structured Field)') 824 | self.logger.debug(f'\tData = {data}') 825 | 826 | partition = data[0] 827 | type_ = data[1] 828 | 829 | if type_ == ReadPartitionType.QUERY: 830 | if partition != 0xff: 831 | self.logger.warning(f'Partition should be 0xff for query, received 0x{partition:02x}') 832 | 833 | self._query() 834 | elif type_ == ReadPartitionType.QUERY_LIST: 835 | if partition != 0xff: 836 | self.logger.warning(f'Partition should be 0xff for query list, received 0x{partition:02x}') 837 | 838 | request_type = QueryListRequestType(data[2]) 839 | 840 | request_codes = None 841 | 842 | if request_type != QueryListRequestType.ALL: 843 | request_codes = [QueryCode(code) for code in data[3:]] 844 | 845 | self._query(request_type, request_codes) 846 | else: 847 | raise NotImplementedError(f'Read partition type 0x{type_:02x} not supported') 848 | 849 | def _outbound_3270ds(self, data): 850 | if self.logger.isEnabledFor(logging.DEBUG): 851 | self.logger.debug('Outbound 3270 DS (Structured Field)') 852 | self.logger.debug(f'\tData = {data}') 853 | 854 | partition = data[0] 855 | command = data[1] 856 | 857 | if partition != 0x00: 858 | self.logger.warning(f'Partition 0x{partition:02x} not supported') 859 | 860 | if command == 0xf1: 861 | self._write(WCC(data[2]), parse_orders(data[3:])) 862 | elif command in [0xf5, 0x7e]: 863 | self._erase(command == 0x7e) 864 | self._write(WCC(data[2]), parse_orders(data[3:])) 865 | elif command == 0x6f: 866 | self._erase_all_unprotected() 867 | else: 868 | raise NotImplementedError(f'Outbound 3270 DS command 0x{command:02x} not supported') 869 | 870 | def _query(self, request_type=None, request_codes=None): 871 | if self.logger.isEnabledFor(logging.DEBUG): 872 | self.logger.debug('Query') 873 | self.logger.debug(f'\tRequest Type = {request_type}') 874 | self.logger.debug(f'\tRequest Codes = {request_codes}') 875 | 876 | codes = [ 877 | QueryCode.USABLE_AREA, QueryCode.ALPHANUMERIC_PARTITIONS, 878 | QueryCode.COLOR, QueryCode.HIGHLIGHT, 879 | QueryCode.REPLY_MODES, QueryCode.IMPLICIT_PARTITIONS 880 | ] 881 | 882 | if request_type == QueryListRequestType.LIST: 883 | codes = request_codes 884 | elif request_type == QueryListRequestType.EQUIVALENT_AND_LIST: 885 | if request_codes is not None: 886 | codes = set(codes + request_codes) 887 | 888 | # Generate the replies. 889 | replies = [] 890 | 891 | for code in codes: 892 | reply = None 893 | 894 | if code == QueryCode.USABLE_AREA: 895 | reply = _query_usable_area(self.alternate_dimensions) 896 | elif code == QueryCode.ALPHANUMERIC_PARTITIONS: 897 | reply = _query_alphanumeric_partitions(self.alternate_dimensions) 898 | elif code == QueryCode.COLOR: 899 | reply = _query_color(self.supported_colors) 900 | elif code == QueryCode.HIGHLIGHT: 901 | reply = _query_highlight(self.supported_highlights) 902 | elif code == QueryCode.REPLY_MODES: 903 | reply = _query_reply_modes() 904 | elif code == QueryCode.IMPLICIT_PARTITIONS: 905 | reply = _query_implicit_partitions(self.alternate_dimensions) 906 | 907 | if reply is not None: 908 | replies.append((code, reply)) 909 | 910 | if request_type == QueryListRequestType.LIST and not replies: 911 | replies.append((QueryCode.NULL, None)) 912 | 913 | # Generate the summary reply. 914 | structured_fields = [(StructuredField.QUERY_REPLY, bytes([QueryCode.SUMMARY, QueryCode.SUMMARY] + [code for (code, _) in replies]))] 915 | 916 | # Append the query replies. 917 | for (code, data) in replies: 918 | structured_fields.append((StructuredField.QUERY_REPLY, bytes([code]) + (data if data is not None else bytes([])))) 919 | 920 | bytes_ = format_inbound_structured_fields(structured_fields) 921 | 922 | if self.logger.isEnabledFor(logging.DEBUG): 923 | self.logger.debug(f'\tData = {bytes_}') 924 | 925 | self.stream.write(bytes_) 926 | 927 | def _query_usable_area(dimensions): 928 | (rows, columns) = dimensions 929 | 930 | return struct.pack('>BBHHBHHHHBBH', 931 | 0x01, # 12/14-bit addressing allowed 932 | 0x00, # Cell units, no special features 933 | columns, # Width in cells 934 | rows, # Width in rows 935 | 0x01, # Millimeters 936 | 10, # X resolution fraction numerator 937 | 741, # X resolution fraction denominator 938 | 2, # Y resolution fraction numerator 939 | 111, # Y resolution fraction denominator 940 | 9, # Width of cell in millimeters 941 | 12, # Height of cell in millimeters 942 | rows * columns # Buffer size 943 | ) 944 | 945 | def _query_alphanumeric_partitions(dimensions): 946 | (rows, columns) = dimensions 947 | 948 | return struct.pack('>BHB', 949 | 1, # One partition 950 | rows * columns, # Buffer size 951 | 0x00 # No special features 952 | ) 953 | 954 | def _query_color(supported_colors): 955 | reply = struct.pack('>BBBB', 956 | 0x00, # Flags 957 | 8, # Colors 958 | 0x00, # Default 959 | Color.GREEN 960 | ) 961 | 962 | for attribute in range(0xf1, 0xf7 + 1): 963 | reply += struct.pack('>BB', attribute, attribute if supported_colors > 4 else 0x00) 964 | 965 | return reply 966 | 967 | def _query_highlight(supported_highlights): 968 | count = len(supported_highlights) + 1 969 | 970 | reply = struct.pack('>BBB', 971 | count, # Types 972 | 0x00, # Default 973 | Highlight.NORMAL 974 | ) 975 | 976 | for attribute in supported_highlights: 977 | reply += struct.pack('>BB', attribute, attribute) 978 | 979 | return reply 980 | 981 | def _query_reply_modes(): 982 | # TODO: Extended field mode is not really supported, but this is enough to trick z/VM 983 | # into sending us pretty screens. 984 | return struct.pack('>BB', 985 | 0x00, # Field mode 986 | 0x01 # Extended field mode 987 | ) 988 | 989 | def _query_implicit_partitions(dimensions): 990 | (rows, columns) = dimensions 991 | 992 | return struct.pack('>HBBBHHHH', 993 | 0x0000, # Flags 994 | 0x0b, # Length 995 | 0x01, # Size 996 | 0x00, # Flags 997 | 80, # Width 998 | 24, # Height 999 | columns, # Width 1000 | rows # Height 1001 | ) 1002 | -------------------------------------------------------------------------------- /tn3270/structured_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.structured_fields 3 | ~~~~~~~~~~~~~~~~~~~~~~~~ 4 | """ 5 | 6 | from enum import Enum, IntEnum 7 | 8 | class StructuredField(IntEnum): 9 | READ_PARTITION = 0x01 10 | ERASE_RESET = 0x03 11 | OUTBOUND_3270DS = 0x40 12 | QUERY_REPLY = 0x81 13 | 14 | class ReadPartitionType(IntEnum): 15 | QUERY = 0x02 16 | QUERY_LIST = 0x03 17 | 18 | class QueryListRequestType(IntEnum): 19 | LIST = 0x00 20 | EQUIVALENT_AND_LIST = 0x40 21 | ALL = 0x80 22 | 23 | class QueryCode(IntEnum): 24 | SUMMARY = 0x80 25 | USABLE_AREA = 0x81 26 | ALPHANUMERIC_PARTITIONS = 0x84 27 | COLOR = 0x86 28 | HIGHLIGHT = 0x87 29 | REPLY_MODES = 0x88 30 | IMPLICIT_PARTITIONS = 0xa6 31 | NULL = 0xff 32 | -------------------------------------------------------------------------------- /tn3270/telnet.py: -------------------------------------------------------------------------------- 1 | """ 2 | tn3270.telnet 3 | ~~~~~~~~~~~~~ 4 | """ 5 | 6 | from enum import IntEnum 7 | from collections import namedtuple 8 | import struct 9 | import time 10 | import logging 11 | import socket 12 | import selectors 13 | from telnetlib3 import IAC, WILL, WONT, DO, DONT, SB, SE, BINARY, EOR, TTYPE, TN3270E 14 | 15 | # https://tools.ietf.org/html/rfc855 16 | RFC855_EOR = b'\xef' 17 | 18 | # https://tools.ietf.org/html/rfc1091 19 | RFC1091_IS = b'\x00' 20 | RFC1091_SEND = b'\x01' 21 | 22 | # https://tools.ietf.org/html/rfc2355 23 | RFC2355_CONNECT = b'\x01' 24 | RFC2355_DEVICE_TYPE = b'\x02' 25 | RFC2355_FUNCTIONS = b'\x03' 26 | RFC2355_IS = b'\x04' 27 | RFC2355_REJECT = b'\x06' 28 | RFC2355_REQUEST = b'\x07' 29 | RFC2355_SEND = b'\x08' 30 | 31 | TN3270EMessageHeader = namedtuple('TN3270EMessageHeader', ['data_type', 'request_flag', 'response_flag', 'sequence_number']) 32 | 33 | class TN3270EFunction(IntEnum): 34 | BIND_IMAGE = 0x00 35 | DATA_STREAM_CTL = 0x01 36 | RESPONSES = 0x02 37 | SCS_CTL_CODES = 0x03 38 | SYSREQ = 0x04 39 | 40 | class TN3270EDataType(IntEnum): 41 | DATA_3270 = 0x00 42 | DATA_SCS = 0x01 43 | RESPONSE = 0x02 44 | BIND_IMAGE = 0x03 45 | UNBIND = 0x04 46 | DATA_NVT = 0x05 47 | REQUEST = 0x06 48 | DATA_SSCP_LU = 0x07 49 | PRINT_EOJ = 0x08 50 | 51 | class TN3270ERequestFlag(IntEnum): 52 | ERROR_CONDITION_CLEARED = 0x00 53 | 54 | class TN3270EResponseFlag(IntEnum): 55 | NO = 0x00 56 | ERROR = 0x01 57 | ALWAYS = 0x02 58 | 59 | class Telnet: 60 | """TN3270 client.""" 61 | 62 | def __init__(self, terminal_type, is_tn3270e_enabled=True, tn3270e_functions=None): 63 | self.logger = logging.getLogger(__name__) 64 | 65 | self.terminal_type = terminal_type 66 | self.is_tn3270e_enabled = is_tn3270e_enabled 67 | 68 | if tn3270e_functions is not None and not is_tn3270e_enabled: 69 | raise ValueError('TN3270E functions not valid if TN3270E is not enabled') 70 | 71 | self.requested_tn3270e_functions = set(tn3270e_functions if tn3270e_functions is not None else []) 72 | 73 | self.socket = None 74 | self.socket_selector = None 75 | self.eof = None 76 | 77 | self.device_names = None 78 | self.host_options = set() 79 | self.client_options = set() 80 | self.is_tn3270e_negotiated = False 81 | self.device_type = None 82 | self.device_name = None 83 | self.tn3270e_functions = set() 84 | 85 | self.buffer = bytearray() 86 | self.iac_buffer = bytearray() 87 | self.records = [] 88 | self.device_names_stack = None 89 | 90 | def open(self, host, port, device_names=None, tn3270_negotiation_timeout=None, ssl_context=None, ssl_server_hostname=None): 91 | """Open the connection.""" 92 | self.close() 93 | 94 | self.socket = socket.create_connection((host, port)) 95 | 96 | if ssl_context: 97 | self.socket = ssl_context.wrap_socket(self.socket, server_hostname=ssl_server_hostname) 98 | 99 | self.socket_selector = selectors.DefaultSelector() 100 | 101 | self.socket_selector.register(self.socket, selectors.EVENT_READ) 102 | 103 | self.eof = False 104 | 105 | self.device_names = device_names 106 | self.host_options = set() 107 | self.client_options = set() 108 | self.is_tn3270e_negotiated = False 109 | self.device_type = None 110 | self.device_name = None 111 | self.tn3270e_functions = set() 112 | 113 | self.buffer = bytearray() 114 | self.iac_buffer = bytearray() 115 | self.records = [] 116 | self.device_names_stack = None 117 | 118 | self._negotiate_tn3270(timeout=tn3270_negotiation_timeout) 119 | 120 | def close(self): 121 | """Close the connection.""" 122 | if self.socket_selector is not None: 123 | self.socket_selector.unregister(self.socket) 124 | 125 | if self.socket is not None: 126 | self.socket.close() 127 | 128 | self.socket = None 129 | 130 | if self.socket_selector is not None: 131 | self.socket_selector.close() 132 | 133 | self.socket_selector = None 134 | 135 | def read_multiple(self, limit=None, timeout=None): 136 | """Read multiple records.""" 137 | records = self._read_multiple_buffered(limit) 138 | 139 | if records: 140 | return records 141 | 142 | self._read_while(lambda: not self.eof and not self.records, timeout) 143 | 144 | # TODO: Determine what happens to any bytes in the buffer if EOF is 145 | # encountered without EOR - should that yield a record? 146 | if self.eof and self.buffer: 147 | self.logger.warning('EOF encountered with partial record') 148 | 149 | return self._read_multiple_buffered(limit) 150 | 151 | def write(self, record): 152 | """Write a record.""" 153 | 154 | # Add 3270-DATA TN3270E header if in TN3270E mode. 155 | if self.is_tn3270e_negotiated: 156 | record = bytes([0x00, 0x00, 0x00, 0x00, 0x00]) + record 157 | 158 | self.socket.sendall(record.replace(IAC, IAC * 2) + IAC + RFC855_EOR) 159 | 160 | def send_tn3270e_positive_response(self, sequence_number): 161 | """Send a TN3270E positive response message.""" 162 | if not self.is_tn3270e_negotiated: 163 | raise Exception('TN3270E mode not negotiated') 164 | 165 | if TN3270EFunction.RESPONSES not in self.tn3270e_functions: 166 | raise Exception('TN3270E responses not negotiated') 167 | 168 | self.logger.debug(f'Sending TN3270E positive response for {sequence_number}') 169 | 170 | bytes_ = struct.pack('!H', sequence_number) 171 | 172 | self.socket.sendall(b'\x02\x00\x00' + bytes_.replace(IAC, IAC * 2) + b'\x00' + IAC + RFC855_EOR) 173 | 174 | def send_tn3270e_negative_response(self, sequence_number, reason): 175 | """Send a TN3270E negative response message.""" 176 | if not self.is_tn3270e_negotiated: 177 | raise Exception('TN3270E mode not negotiated') 178 | 179 | if TN3270EFunction.RESPONSES not in self.tn3270e_functions: 180 | raise Exception('TN3270E responses not negotiated') 181 | 182 | self.logger.debug(f'Sending TN3270E negative response for {sequence_number}, reason = {reason}') 183 | 184 | bytes_ = struct.pack('!HB', sequence_number, reason) 185 | 186 | self.socket.sendall(b'\x02\x00\x01' + bytes_.replace(IAC, IAC * 2) + IAC + RFC855_EOR) 187 | 188 | @property 189 | def is_tn3270_negotiated(self): 190 | """Has TN3270 or TN3270E mode been negotiated.""" 191 | 192 | if self.is_tn3270e_negotiated: 193 | return True 194 | 195 | # https://tools.ietf.org/html/rfc1576 196 | return (self.client_options.issuperset([BINARY, EOR, TTYPE]) 197 | and self.host_options.issuperset([BINARY, EOR])) 198 | 199 | def _read(self, timeout): 200 | if self.eof: 201 | raise EOFError 202 | 203 | if not self.socket_selector.select(timeout): 204 | return 205 | 206 | bytes_ = self.socket.recv(1024) 207 | 208 | if not bytes_: 209 | self.eof = True 210 | return 211 | 212 | for byte in bytes_: 213 | self._feed(bytes([byte])) 214 | 215 | def _read_while(self, predicate, timeout): 216 | remaining_timeout = timeout 217 | 218 | while predicate(): 219 | read_time = time.perf_counter() 220 | 221 | self._read(remaining_timeout) 222 | 223 | if remaining_timeout is not None: 224 | remaining_timeout -= (time.perf_counter() - read_time) 225 | 226 | if remaining_timeout < 0: 227 | break 228 | 229 | def _read_multiple_buffered(self, limit=None): 230 | if self.eof and not self.records: 231 | raise EOFError 232 | 233 | if not self.records: 234 | return [] 235 | 236 | count = limit if limit is not None else len(self.records) 237 | 238 | records = self.records[:count] 239 | 240 | self.records = self.records[count:] 241 | 242 | return records 243 | 244 | def _feed(self, byte): 245 | if not self.iac_buffer: 246 | if byte == IAC: 247 | self.iac_buffer += byte 248 | return 249 | 250 | self.buffer += byte 251 | elif len(self.iac_buffer) == 1: 252 | if byte == IAC: 253 | self.buffer += IAC 254 | self.iac_buffer.clear() 255 | return 256 | 257 | if byte == RFC855_EOR: 258 | self._eor(bytearray(self.buffer)) 259 | 260 | self.buffer.clear() 261 | self.iac_buffer.clear() 262 | return 263 | 264 | if byte in [WILL, WONT, DO, DONT, SB]: 265 | self.iac_buffer += byte 266 | return 267 | 268 | self.logger.warning(f'Unexpected byte 0x{byte[0]:02x} in IAC state') 269 | 270 | self.iac_buffer.clear() 271 | elif len(self.iac_buffer) > 1: 272 | command = self.iac_buffer[1:2] 273 | 274 | if command in [WILL, WONT, DO, DONT]: 275 | self._handle_negotiation(command, byte) 276 | 277 | self.iac_buffer.clear() 278 | return 279 | 280 | if command == SB: 281 | if byte == SE: 282 | if self.iac_buffer[-1:] != IAC: 283 | self.logger.warning('Expected IAC prior to SE') 284 | 285 | self._handle_subnegotiation(self.iac_buffer[2:-1].replace(IAC * 2, IAC)) 286 | 287 | self.iac_buffer.clear() 288 | return 289 | 290 | self.iac_buffer += byte 291 | return 292 | 293 | self.logger.warning(f'Unrecognized command 0x{command:02x}') 294 | 295 | self.iac_buffer.clear() 296 | 297 | def _handle_negotiation(self, command, option): 298 | if self.logger.isEnabledFor(logging.DEBUG): 299 | self.logger.debug((f'Negotiate: Command = 0x{command.hex()}, ' 300 | f'Option = 0x{option.hex()}')) 301 | 302 | if command == WILL: 303 | if option in [BINARY, EOR, TTYPE] or (option == TN3270E and self.is_tn3270e_enabled): 304 | self.host_options.add(option) 305 | 306 | self.socket.sendall(IAC + DO + option) 307 | else: 308 | self.socket.sendall(IAC + DONT + option) 309 | elif command == WONT: 310 | self.host_options.discard(option) 311 | 312 | self.socket.sendall(IAC + DONT + option) 313 | elif command == DO: 314 | if option in [BINARY, EOR, TTYPE] or (option == TN3270E and self.is_tn3270e_enabled): 315 | self.client_options.add(option) 316 | 317 | self.socket.sendall(IAC + WILL + option) 318 | else: 319 | self.socket.sendall(IAC + WONT + option) 320 | elif command == DONT: 321 | self.client_options.discard(option) 322 | 323 | self.socket.sendall(IAC + WONT + option) 324 | 325 | def _handle_subnegotiation(self, bytes_): 326 | if bytes_ == TTYPE + RFC1091_SEND: 327 | self.logger.debug('Received TTYPE SEND request') 328 | 329 | # TN3270E and TTYPE negotation are supposed to be mutually exclusive. 330 | if self.is_tn3270e_negotiated: 331 | self.logger.warning('Unexpected TTYPE SEND request after TN3270E negotation') 332 | 333 | self.device_name = None 334 | 335 | if self.device_names_stack: 336 | self.device_name = self.device_names_stack.pop(0) 337 | 338 | self.logger.debug(f'Trying device name {self.device_name}...') 339 | elif self.device_names_stack is not None: 340 | self.logger.debug('Exhausted device names, continuing with no device name') 341 | 342 | terminal_type = encode_rfc1646_terminal_type(self.terminal_type, self.device_name) 343 | 344 | self.socket.sendall(IAC + SB + TTYPE + RFC1091_IS + terminal_type + IAC + SE) 345 | elif bytes_.startswith(TN3270E): 346 | if not self.is_tn3270e_enabled: 347 | self.logger.warning('TN3270E subnegotiation requested but TN3270E not enabled') 348 | 349 | self._handle_tn3270e_subnegotiation(bytes_[1:]) 350 | 351 | def _handle_tn3270e_subnegotiation(self, bytes_): 352 | # TN3270E and TTYPE negotation are supposed to be mutually exclusive. 353 | if TTYPE in self.client_options: 354 | self.logger.warning('Unexpected TN3270E negotiation after TTYPE') 355 | 356 | if bytes_ == RFC2355_SEND + RFC2355_DEVICE_TYPE: 357 | self.logger.debug('Received TN3270E SEND DEVICE-TYPE request') 358 | 359 | self._send_tn3270e_device_type() 360 | elif bytes_.startswith(RFC2355_DEVICE_TYPE + RFC2355_IS): 361 | self.logger.debug('Received TN3270E DEVICE-TYPE response') 362 | 363 | (self.device_type, self.device_name) = decode_rfc2355_device_type(bytes_[2:]) 364 | 365 | self._send_tn3270e_functions(RFC2355_REQUEST, self.requested_tn3270e_functions) 366 | elif bytes_.startswith(RFC2355_DEVICE_TYPE + RFC2355_REJECT): 367 | self.logger.debug('Received TN3270E DEVICE-TYPE REJECT response') 368 | 369 | self.device_name = None 370 | 371 | # Try the next device name, or reset the stack for TTYPE negotation. 372 | if self.device_names_stack: 373 | self._send_tn3270e_device_type() 374 | else: 375 | if self.device_names_stack is not None: 376 | self.logger.debug('Exhausted device names, continuing without TN3270E') 377 | else: 378 | self.logger.debug('Continuing without TN3270E') 379 | 380 | self._reset_device_names_stack() 381 | 382 | self.socket.sendall(IAC + WONT + TN3270E) 383 | elif bytes_.startswith(RFC2355_FUNCTIONS + RFC2355_REQUEST): 384 | self.logger.debug('Received TN3270E FUNCTIONS request') 385 | 386 | functions = set([TN3270EFunction(byte_) for byte_ in bytes_[2:]]) 387 | 388 | if functions.issubset(self.requested_tn3270e_functions): 389 | self.tn3270e_functions = functions 390 | 391 | self._send_tn3270e_functions(RFC2355_IS, functions) 392 | 393 | self.logger.debug('TN3270E negotiation complete') 394 | 395 | self.is_tn3270e_negotiated = True 396 | else: 397 | common_functions = functions.intersection(self.requested_tn3270e_functions) 398 | 399 | self._send_tn3270e_functions(RFC2355_REQUEST, common_functions) 400 | elif bytes_.startswith(RFC2355_FUNCTIONS + RFC2355_IS): 401 | self.logger.debug('Received TN3270E FUNCTIONS response') 402 | 403 | functions = set([TN3270EFunction(byte_) for byte_ in bytes_[2:]]) 404 | 405 | if functions.issubset(self.requested_tn3270e_functions): 406 | self.tn3270e_functions = functions 407 | 408 | self._send_tn3270e_functions(RFC2355_IS, functions) 409 | 410 | self.logger.debug('TN3270E negotiation complete') 411 | 412 | self.is_tn3270e_negotiated = True 413 | else: 414 | self.logger.warning('TN3270E FUNCTIONS response contains unrequested functions, aborting TN3270E negotiation') 415 | 416 | self.socket.sendall(IAC + WONT + TN3270E) 417 | 418 | def _reset_device_names_stack(self): 419 | # Clone the device names as the stack will be mutated during negotiation. 420 | self.device_names_stack = list(self.device_names) if self.device_names is not None else None 421 | 422 | def _send_tn3270e_device_type(self): 423 | device_type = self.terminal_type.replace('IBM-3279', 'IBM-3278') 424 | 425 | self.device_name = None 426 | 427 | if self.device_names_stack: 428 | self.device_name = self.device_names_stack.pop(0) 429 | 430 | self.logger.debug(f'Trying device name {self.device_name}...') 431 | 432 | bytes_ = encode_rfc2355_device_type(device_type, self.device_name) 433 | 434 | self.socket.sendall(IAC + SB + TN3270E + RFC2355_DEVICE_TYPE + RFC2355_REQUEST + bytes_ + IAC + SE) 435 | 436 | def _send_tn3270e_functions(self, command, functions): 437 | bytes_ = bytes(functions) 438 | 439 | self.socket.sendall(IAC + SB + TN3270E + RFC2355_FUNCTIONS + command + bytes_ + IAC + SE) 440 | 441 | def _negotiate_tn3270(self, timeout): 442 | self._reset_device_names_stack() 443 | 444 | self._read_while(lambda: not self.is_tn3270_negotiated and not self.eof 445 | and not self.buffer, timeout) 446 | 447 | if not self.is_tn3270_negotiated: 448 | raise Exception('Unable to negotiate TN3270 mode') 449 | 450 | def _eor(self, record): 451 | self.logger.debug('Received EOR') 452 | 453 | if self.is_tn3270e_negotiated: 454 | header = decode_tn3270e_message_header(record[:5]) 455 | data = record[5:] 456 | 457 | self.records.append((data, header)) 458 | else: 459 | self.records.append((record, None)) 460 | 461 | def __del__(self): 462 | self.close() 463 | 464 | def encode_rfc1646_terminal_type(terminal_type, device_name): 465 | bytes_ = terminal_type.encode('ascii') 466 | 467 | if device_name is not None: 468 | bytes_ += f'@{device_name}'.encode('ascii') 469 | 470 | return bytes_ 471 | 472 | def encode_rfc2355_device_type(device_type, device_name): 473 | bytes_ = device_type.encode('ascii') 474 | 475 | if device_name is not None: 476 | bytes_ += RFC2355_CONNECT + device_name.encode('ascii') 477 | 478 | return bytes_ 479 | 480 | def decode_rfc2355_device_type(bytes_): 481 | elements = bytes_.split(RFC2355_CONNECT, 1) 482 | 483 | device_type = elements[0].decode('ascii') 484 | device_name = elements[1].decode('ascii') if len(elements) > 1 else None 485 | 486 | return (device_type, device_name) 487 | 488 | def decode_tn3270e_message_header(bytes_): 489 | (data_type, request_flag, response_flag, sequence_number) = struct.unpack('!BBBH', bytes_) 490 | 491 | data_type = TN3270EDataType(data_type) 492 | 493 | request_flag = TN3270ERequestFlag(request_flag) if data_type == TN3270EDataType.REQUEST else None 494 | 495 | response_flag = TN3270EResponseFlag(response_flag) if data_type in (TN3270EDataType.DATA_3270, TN3270EDataType.DATA_SCS) else None 496 | 497 | return TN3270EMessageHeader(data_type, request_flag, response_flag, sequence_number) 498 | --------------------------------------------------------------------------------