green-screen-proxy 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/cli.d.ts +2 -0
  2. package/dist/cli.js +32 -0
  3. package/dist/hp6530/connection.d.ts +51 -0
  4. package/dist/hp6530/connection.js +258 -0
  5. package/dist/hp6530/constants.d.ts +64 -0
  6. package/dist/hp6530/constants.js +135 -0
  7. package/dist/hp6530/encoder.d.ts +37 -0
  8. package/dist/hp6530/encoder.js +89 -0
  9. package/dist/hp6530/parser.d.ts +45 -0
  10. package/dist/hp6530/parser.js +255 -0
  11. package/dist/hp6530/screen.d.ts +104 -0
  12. package/dist/hp6530/screen.js +252 -0
  13. package/dist/mock/mock-routes.d.ts +2 -0
  14. package/dist/mock/mock-routes.js +231 -0
  15. package/dist/protocols/hp6530-handler.d.ts +29 -0
  16. package/dist/protocols/hp6530-handler.js +64 -0
  17. package/dist/protocols/index.d.ts +11 -0
  18. package/dist/protocols/index.js +27 -0
  19. package/dist/protocols/tn3270-handler.d.ts +26 -0
  20. package/dist/protocols/tn3270-handler.js +61 -0
  21. package/dist/protocols/tn5250-handler.d.ts +26 -0
  22. package/dist/protocols/tn5250-handler.js +62 -0
  23. package/dist/protocols/types.d.ts +59 -0
  24. package/dist/protocols/types.js +7 -0
  25. package/dist/protocols/vt-handler.d.ts +30 -0
  26. package/dist/protocols/vt-handler.js +67 -0
  27. package/dist/routes.d.ts +2 -0
  28. package/dist/routes.js +141 -0
  29. package/dist/server.d.ts +1 -0
  30. package/dist/server.js +34 -0
  31. package/dist/session.d.ts +32 -0
  32. package/dist/session.js +88 -0
  33. package/dist/tn3270/connection.d.ts +31 -0
  34. package/dist/tn3270/connection.js +266 -0
  35. package/dist/tn3270/constants.d.ts +262 -0
  36. package/dist/tn3270/constants.js +261 -0
  37. package/dist/tn3270/encoder.d.ts +24 -0
  38. package/dist/tn3270/encoder.js +97 -0
  39. package/dist/tn3270/parser.d.ts +22 -0
  40. package/dist/tn3270/parser.js +284 -0
  41. package/dist/tn3270/screen.d.ts +89 -0
  42. package/dist/tn3270/screen.js +207 -0
  43. package/dist/tn5250/connection.d.ts +41 -0
  44. package/dist/tn5250/connection.js +254 -0
  45. package/dist/tn5250/constants.d.ts +128 -0
  46. package/dist/tn5250/constants.js +156 -0
  47. package/dist/tn5250/ebcdic.d.ts +10 -0
  48. package/dist/tn5250/ebcdic.js +89 -0
  49. package/dist/tn5250/encoder.d.ts +30 -0
  50. package/dist/tn5250/encoder.js +121 -0
  51. package/dist/tn5250/parser.d.ts +33 -0
  52. package/dist/tn5250/parser.js +412 -0
  53. package/dist/tn5250/screen.d.ts +80 -0
  54. package/dist/tn5250/screen.js +155 -0
  55. package/dist/vt/connection.d.ts +45 -0
  56. package/dist/vt/connection.js +229 -0
  57. package/dist/vt/constants.d.ts +97 -0
  58. package/dist/vt/constants.js +163 -0
  59. package/dist/vt/encoder.d.ts +30 -0
  60. package/dist/vt/encoder.js +55 -0
  61. package/dist/vt/parser.d.ts +36 -0
  62. package/dist/vt/parser.js +534 -0
  63. package/dist/vt/screen.d.ts +101 -0
  64. package/dist/vt/screen.js +424 -0
  65. package/dist/websocket.d.ts +6 -0
  66. package/dist/websocket.js +50 -0
  67. package/package.json +57 -0
@@ -0,0 +1,67 @@
1
+ import { ProtocolHandler } from './types.js';
2
+ import { VTConnection } from '../vt/connection.js';
3
+ import { VTScreenBuffer } from '../vt/screen.js';
4
+ import { VTParser } from '../vt/parser.js';
5
+ import { VTEncoder } from '../vt/encoder.js';
6
+ /**
7
+ * VT terminal protocol handler — implements the ProtocolHandler interface
8
+ * for VT100/VT220/VT320 terminal connections.
9
+ *
10
+ * VT terminals are stream-mode (character-at-a-time). Each keystroke is
11
+ * sent immediately; the host echoes characters back. Used by OpenVMS,
12
+ * Pick/MultiValue, Unix, and many other systems.
13
+ */
14
+ export class VTHandler extends ProtocolHandler {
15
+ protocol = 'vt';
16
+ connection;
17
+ screen;
18
+ parser;
19
+ encoder;
20
+ constructor() {
21
+ super();
22
+ this.screen = new VTScreenBuffer();
23
+ this.connection = new VTConnection();
24
+ this.parser = new VTParser(this.screen);
25
+ this.encoder = new VTEncoder(this.screen);
26
+ this.connection.on('data', (data) => this.onData(data));
27
+ this.connection.on('disconnected', () => this.emit('disconnected'));
28
+ this.connection.on('error', (err) => this.emit('error', err));
29
+ }
30
+ get isConnected() {
31
+ return this.connection.isConnected;
32
+ }
33
+ async connect(host, port, _options) {
34
+ await this.connection.connect(host, port);
35
+ }
36
+ disconnect() {
37
+ this.connection.disconnect();
38
+ }
39
+ getScreenData() {
40
+ return this.screen.toScreenData();
41
+ }
42
+ sendText(text) {
43
+ const encoded = this.encoder.encodeText(text);
44
+ this.connection.sendRaw(encoded);
45
+ return true;
46
+ }
47
+ sendKey(keyName) {
48
+ const encoded = this.encoder.encodeKey(keyName);
49
+ if (!encoded)
50
+ return false;
51
+ this.connection.sendRaw(encoded);
52
+ return true;
53
+ }
54
+ sendRaw(data) {
55
+ this.connection.sendRaw(data);
56
+ }
57
+ destroy() {
58
+ this.disconnect();
59
+ this.removeAllListeners();
60
+ }
61
+ onData(data) {
62
+ const modified = this.parser.feed(data);
63
+ if (modified) {
64
+ this.emit('screenChange', this.screen.toScreenData());
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,2 @@
1
+ declare const router: import("express-serve-static-core").Router;
2
+ export default router;
package/dist/routes.js ADDED
@@ -0,0 +1,141 @@
1
+ import { Router } from 'express';
2
+ import { createSession, getSession, getDefaultSession, destroySession, } from './session.js';
3
+ import { bindSessionToWebSocket } from './websocket.js';
4
+ const router = Router();
5
+ /** Resolve session from header, query param, or default */
6
+ function resolveSession(req) {
7
+ const sessionId = req.headers['x-session-id'] ||
8
+ req.query.sessionId;
9
+ if (sessionId) {
10
+ return getSession(sessionId);
11
+ }
12
+ return getDefaultSession();
13
+ }
14
+ // POST /connect
15
+ router.post('/connect', async (req, res) => {
16
+ try {
17
+ const { host = 'pub400.com', port = 23, protocol = 'tn5250' } = req.body || {};
18
+ const session = createSession(protocol);
19
+ // Store session ID in response header
20
+ res.setHeader('X-Session-Id', session.id);
21
+ bindSessionToWebSocket(session);
22
+ await session.connect(host, port);
23
+ // Wait briefly for initial screen data
24
+ await new Promise(resolve => setTimeout(resolve, 2000));
25
+ const screenData = session.getScreenData();
26
+ res.json({
27
+ success: true,
28
+ sessionId: session.id,
29
+ cursor_row: screenData.cursor_row,
30
+ cursor_col: screenData.cursor_col,
31
+ content: screenData.content,
32
+ screen_signature: screenData.screen_signature,
33
+ });
34
+ }
35
+ catch (err) {
36
+ res.status(500).json({
37
+ success: false,
38
+ error: err.message || 'Connection failed',
39
+ });
40
+ }
41
+ });
42
+ // POST /disconnect
43
+ router.post('/disconnect', (req, res) => {
44
+ const session = resolveSession(req);
45
+ if (!session) {
46
+ return res.json({ success: true }); // Already disconnected
47
+ }
48
+ destroySession(session.id);
49
+ res.json({ success: true });
50
+ });
51
+ // POST /reconnect
52
+ router.post('/reconnect', async (req, res) => {
53
+ const session = resolveSession(req);
54
+ if (!session) {
55
+ return res.status(404).json({ success: false, error: 'No active session' });
56
+ }
57
+ try {
58
+ await session.reconnect();
59
+ await new Promise(resolve => setTimeout(resolve, 2000));
60
+ const screenData = session.getScreenData();
61
+ res.json({
62
+ success: true,
63
+ cursor_row: screenData.cursor_row,
64
+ cursor_col: screenData.cursor_col,
65
+ content: screenData.content,
66
+ screen_signature: screenData.screen_signature,
67
+ });
68
+ }
69
+ catch (err) {
70
+ res.status(500).json({ success: false, error: err.message });
71
+ }
72
+ });
73
+ // GET /status
74
+ router.get('/status', (req, res) => {
75
+ const session = resolveSession(req);
76
+ if (!session) {
77
+ return res.json({
78
+ connected: false,
79
+ status: 'disconnected',
80
+ });
81
+ }
82
+ res.json(session.status);
83
+ });
84
+ // GET /screen
85
+ router.get('/screen', (req, res) => {
86
+ const session = resolveSession(req);
87
+ if (!session) {
88
+ return res.status(503).json(null);
89
+ }
90
+ if (!session.status.connected) {
91
+ return res.status(503).json(null);
92
+ }
93
+ res.json(session.getScreenData());
94
+ });
95
+ // POST /send-text
96
+ router.post('/send-text', (req, res) => {
97
+ const session = resolveSession(req);
98
+ if (!session) {
99
+ return res.status(404).json({ success: false, error: 'No active session' });
100
+ }
101
+ const { text } = req.body || {};
102
+ if (typeof text !== 'string') {
103
+ return res.status(400).json({ success: false, error: 'text is required' });
104
+ }
105
+ const ok = session.sendText(text);
106
+ const screenData = session.getScreenData();
107
+ res.json({
108
+ success: ok,
109
+ cursor_row: screenData.cursor_row,
110
+ cursor_col: screenData.cursor_col,
111
+ content: screenData.content,
112
+ screen_signature: screenData.screen_signature,
113
+ error: ok ? undefined : 'Cannot type at current cursor position',
114
+ });
115
+ });
116
+ // POST /send-key
117
+ router.post('/send-key', async (req, res) => {
118
+ const session = resolveSession(req);
119
+ if (!session) {
120
+ return res.status(404).json({ success: false, error: 'No active session' });
121
+ }
122
+ const { key } = req.body || {};
123
+ if (typeof key !== 'string') {
124
+ return res.status(400).json({ success: false, error: 'key is required' });
125
+ }
126
+ const ok = session.sendKey(key);
127
+ if (!ok) {
128
+ return res.json({ success: false, error: `Unknown key: ${key}` });
129
+ }
130
+ // Wait for the host to respond with a new screen
131
+ await new Promise(resolve => setTimeout(resolve, 1500));
132
+ const screenData = session.getScreenData();
133
+ res.json({
134
+ success: true,
135
+ cursor_row: screenData.cursor_row,
136
+ cursor_col: screenData.cursor_col,
137
+ content: screenData.content,
138
+ screen_signature: screenData.screen_signature,
139
+ });
140
+ });
141
+ export default router;
@@ -0,0 +1 @@
1
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,34 @@
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import { createServer } from 'http';
4
+ const isMock = process.argv.includes('--mock');
5
+ const PORT = parseInt(process.env.PORT || '3001', 10);
6
+ const app = express();
7
+ app.use(cors());
8
+ app.use(express.json());
9
+ if (isMock) {
10
+ import('./mock/mock-routes.js').then(({ default: mockRoutes }) => {
11
+ app.use('/', mockRoutes);
12
+ startServer();
13
+ });
14
+ }
15
+ else {
16
+ Promise.all([
17
+ import('./routes.js'),
18
+ import('./websocket.js'),
19
+ ]).then(([{ default: routes }, { setupWebSocket }]) => {
20
+ app.use('/', routes);
21
+ startServer(setupWebSocket);
22
+ });
23
+ }
24
+ function startServer(setupWebSocket) {
25
+ const server = createServer(app);
26
+ if (setupWebSocket)
27
+ setupWebSocket(server);
28
+ server.listen(PORT, () => {
29
+ console.log(`Green Screen proxy server running on http://localhost:${PORT}`);
30
+ if (isMock) {
31
+ console.log('Running in MOCK mode (no real IBM i connection)');
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,32 @@
1
+ import { EventEmitter } from 'events';
2
+ import { ProtocolHandler, ProtocolType, ScreenData } from './protocols/index.js';
3
+ export interface SessionStatus {
4
+ connected: boolean;
5
+ status: 'disconnected' | 'connecting' | 'connected' | 'authenticated' | 'error';
6
+ protocol?: ProtocolType;
7
+ host?: string;
8
+ username?: string;
9
+ error?: string;
10
+ }
11
+ export declare class Session extends EventEmitter {
12
+ readonly id: string;
13
+ readonly handler: ProtocolHandler;
14
+ readonly protocol: ProtocolType;
15
+ private _status;
16
+ private _host;
17
+ private _port;
18
+ constructor(protocol?: ProtocolType);
19
+ get status(): SessionStatus;
20
+ connect(host: string, port: number): Promise<void>;
21
+ disconnect(): void;
22
+ reconnect(): Promise<void>;
23
+ sendText(text: string): boolean;
24
+ sendKey(keyName: string): boolean;
25
+ getScreenData(): ScreenData;
26
+ destroy(): void;
27
+ }
28
+ export declare function createSession(protocol?: ProtocolType): Session;
29
+ export declare function getSession(id: string): Session | undefined;
30
+ export declare function destroySession(id: string): void;
31
+ export declare function getDefaultSession(): Session | undefined;
32
+ export declare function getAllSessions(): Map<string, Session>;
@@ -0,0 +1,88 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { EventEmitter } from 'events';
3
+ import { createProtocolHandler } from './protocols/index.js';
4
+ export class Session extends EventEmitter {
5
+ id;
6
+ handler;
7
+ protocol;
8
+ _status = { connected: false, status: 'disconnected' };
9
+ _host = '';
10
+ _port = 23;
11
+ constructor(protocol = 'tn5250') {
12
+ super();
13
+ this.id = randomUUID();
14
+ this.protocol = protocol;
15
+ this.handler = createProtocolHandler(protocol);
16
+ this.handler.on('screenChange', (screenData) => {
17
+ this.emit('screenChange', screenData);
18
+ });
19
+ this.handler.on('disconnected', () => {
20
+ this._status = { connected: false, status: 'disconnected', protocol: this.protocol, host: this._host };
21
+ this.emit('statusChange', this._status);
22
+ });
23
+ this.handler.on('error', (err) => {
24
+ this._status = { connected: false, status: 'error', protocol: this.protocol, host: this._host, error: err.message };
25
+ this.emit('statusChange', this._status);
26
+ });
27
+ }
28
+ get status() {
29
+ return { ...this._status };
30
+ }
31
+ async connect(host, port) {
32
+ this._host = host;
33
+ this._port = port;
34
+ this._status = { connected: false, status: 'connecting', protocol: this.protocol, host };
35
+ this.emit('statusChange', this._status);
36
+ await this.handler.connect(host, port);
37
+ this._status = { connected: true, status: 'connected', protocol: this.protocol, host };
38
+ this.emit('statusChange', this._status);
39
+ }
40
+ disconnect() {
41
+ this.handler.disconnect();
42
+ this._status = { connected: false, status: 'disconnected', protocol: this.protocol, host: this._host };
43
+ this.emit('statusChange', this._status);
44
+ }
45
+ async reconnect() {
46
+ this.disconnect();
47
+ await this.connect(this._host, this._port);
48
+ }
49
+ sendText(text) {
50
+ return this.handler.sendText(text);
51
+ }
52
+ sendKey(keyName) {
53
+ return this.handler.sendKey(keyName);
54
+ }
55
+ getScreenData() {
56
+ return this.handler.getScreenData();
57
+ }
58
+ destroy() {
59
+ this.handler.destroy();
60
+ this.removeAllListeners();
61
+ }
62
+ }
63
+ // Session manager
64
+ const sessions = new Map();
65
+ export function createSession(protocol = 'tn5250') {
66
+ const session = new Session(protocol);
67
+ sessions.set(session.id, session);
68
+ return session;
69
+ }
70
+ export function getSession(id) {
71
+ return sessions.get(id);
72
+ }
73
+ export function destroySession(id) {
74
+ const session = sessions.get(id);
75
+ if (session) {
76
+ session.destroy();
77
+ sessions.delete(id);
78
+ }
79
+ }
80
+ export function getDefaultSession() {
81
+ if (sessions.size === 1) {
82
+ return sessions.values().next().value;
83
+ }
84
+ return undefined;
85
+ }
86
+ export function getAllSessions() {
87
+ return sessions;
88
+ }
@@ -0,0 +1,31 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * Manages raw TCP socket to a z/OS (or other 3270) host.
4
+ * Handles Telnet negotiation and extracts 3270 data records (IAC EOR delimited).
5
+ *
6
+ * Supports basic TN3270 (RFC 1576) negotiation.
7
+ * TN3270E (RFC 2355) is handled at a basic level.
8
+ */
9
+ export declare class TN3270Connection extends EventEmitter {
10
+ private socket;
11
+ private host;
12
+ private port;
13
+ private connected;
14
+ private recvBuffer;
15
+ private tn3270eMode;
16
+ get isConnected(): boolean;
17
+ connect(host: string, port: number): Promise<void>;
18
+ disconnect(): void;
19
+ sendRaw(data: Buffer): void;
20
+ private cleanup;
21
+ private onData;
22
+ private processBuffer;
23
+ private findSubnegEnd;
24
+ private findRecordEnd;
25
+ private unescapeIAC;
26
+ private handleNegotiation;
27
+ private handleSubnegotiation;
28
+ private sendTerminalType;
29
+ private handleTN3270ESubneg;
30
+ private sendTelnet;
31
+ }
@@ -0,0 +1,266 @@
1
+ import * as net from 'net';
2
+ import { EventEmitter } from 'events';
3
+ import { TELNET } from '../tn5250/constants.js';
4
+ import { TERMINAL_TYPE } from './constants.js';
5
+ /**
6
+ * Manages raw TCP socket to a z/OS (or other 3270) host.
7
+ * Handles Telnet negotiation and extracts 3270 data records (IAC EOR delimited).
8
+ *
9
+ * Supports basic TN3270 (RFC 1576) negotiation.
10
+ * TN3270E (RFC 2355) is handled at a basic level.
11
+ */
12
+ export class TN3270Connection extends EventEmitter {
13
+ socket = null;
14
+ host = '';
15
+ port = 23;
16
+ connected = false;
17
+ recvBuffer = Buffer.alloc(0);
18
+ tn3270eMode = false;
19
+ get isConnected() {
20
+ return this.connected;
21
+ }
22
+ connect(host, port) {
23
+ return new Promise((resolve, reject) => {
24
+ if (this.socket) {
25
+ this.disconnect();
26
+ }
27
+ this.host = host;
28
+ this.port = port;
29
+ this.recvBuffer = Buffer.alloc(0);
30
+ this.tn3270eMode = false;
31
+ this.socket = new net.Socket();
32
+ this.socket.setTimeout(30000);
33
+ const onError = (err) => {
34
+ this.cleanup();
35
+ reject(err);
36
+ };
37
+ this.socket.once('error', onError);
38
+ this.socket.connect(port, host, () => {
39
+ this.connected = true;
40
+ this.socket.removeListener('error', onError);
41
+ this.socket.on('error', (err) => {
42
+ this.emit('error', err);
43
+ this.cleanup();
44
+ });
45
+ this.socket.on('close', () => {
46
+ this.cleanup();
47
+ this.emit('disconnected');
48
+ });
49
+ this.socket.on('timeout', () => {
50
+ this.emit('error', new Error('Connection timeout'));
51
+ });
52
+ this.socket.on('data', (data) => this.onData(data));
53
+ this.emit('connected');
54
+ resolve();
55
+ });
56
+ });
57
+ }
58
+ disconnect() {
59
+ if (this.socket) {
60
+ this.socket.destroy();
61
+ this.cleanup();
62
+ }
63
+ }
64
+ sendRaw(data) {
65
+ if (this.socket && this.connected) {
66
+ this.socket.write(data);
67
+ }
68
+ }
69
+ cleanup() {
70
+ this.connected = false;
71
+ if (this.socket) {
72
+ this.socket.removeAllListeners();
73
+ this.socket.destroy();
74
+ this.socket = null;
75
+ }
76
+ }
77
+ onData(data) {
78
+ this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
79
+ this.processBuffer();
80
+ }
81
+ processBuffer() {
82
+ while (this.recvBuffer.length > 0) {
83
+ // Check for Telnet commands
84
+ if (this.recvBuffer[0] === TELNET.IAC && this.recvBuffer.length >= 2) {
85
+ const cmd = this.recvBuffer[1];
86
+ if (cmd === TELNET.IAC) {
87
+ // Escaped 0xFF — part of data stream
88
+ break;
89
+ }
90
+ // Subnegotiation
91
+ if (cmd === TELNET.SB) {
92
+ const seIdx = this.findSubnegEnd();
93
+ if (seIdx === -1)
94
+ return;
95
+ const subData = this.recvBuffer.subarray(2, seIdx);
96
+ this.recvBuffer = this.recvBuffer.subarray(seIdx + 2);
97
+ this.handleSubnegotiation(subData);
98
+ continue;
99
+ }
100
+ // DO/DONT/WILL/WONT
101
+ if (cmd === TELNET.DO || cmd === TELNET.DONT || cmd === TELNET.WILL || cmd === TELNET.WONT) {
102
+ if (this.recvBuffer.length < 3)
103
+ return;
104
+ const option = this.recvBuffer[2];
105
+ this.recvBuffer = this.recvBuffer.subarray(3);
106
+ this.handleNegotiation(cmd, option);
107
+ continue;
108
+ }
109
+ if (cmd === TELNET.EOR) {
110
+ this.recvBuffer = this.recvBuffer.subarray(2);
111
+ continue;
112
+ }
113
+ // Skip unknown 2-byte commands
114
+ this.recvBuffer = this.recvBuffer.subarray(2);
115
+ continue;
116
+ }
117
+ // Extract a 3270 record: data terminated by IAC EOR
118
+ const recordEnd = this.findRecordEnd();
119
+ if (recordEnd === -1)
120
+ return;
121
+ const rawRecord = this.recvBuffer.subarray(0, recordEnd);
122
+ this.recvBuffer = this.recvBuffer.subarray(recordEnd + 2);
123
+ const record = this.unescapeIAC(rawRecord);
124
+ if (record.length > 0) {
125
+ // In TN3270E mode, strip the 5-byte header
126
+ if (this.tn3270eMode && record.length > 5) {
127
+ const dataRecord = record.subarray(5);
128
+ if (dataRecord.length > 0) {
129
+ this.emit('data', dataRecord);
130
+ }
131
+ }
132
+ else {
133
+ this.emit('data', record);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ findSubnegEnd() {
139
+ for (let i = 2; i < this.recvBuffer.length - 1; i++) {
140
+ if (this.recvBuffer[i] === TELNET.IAC && this.recvBuffer[i + 1] === TELNET.SE) {
141
+ return i;
142
+ }
143
+ }
144
+ return -1;
145
+ }
146
+ findRecordEnd() {
147
+ for (let i = 0; i < this.recvBuffer.length - 1; i++) {
148
+ if (this.recvBuffer[i] === TELNET.IAC && this.recvBuffer[i + 1] === TELNET.EOR) {
149
+ return i;
150
+ }
151
+ }
152
+ return -1;
153
+ }
154
+ unescapeIAC(data) {
155
+ const result = [];
156
+ for (let i = 0; i < data.length; i++) {
157
+ if (data[i] === TELNET.IAC && i + 1 < data.length && data[i + 1] === TELNET.IAC) {
158
+ result.push(TELNET.IAC);
159
+ i++;
160
+ }
161
+ else {
162
+ result.push(data[i]);
163
+ }
164
+ }
165
+ return Buffer.from(result);
166
+ }
167
+ handleNegotiation(cmd, option) {
168
+ switch (cmd) {
169
+ case TELNET.DO:
170
+ if (option === TELNET.OPT_TTYPE ||
171
+ option === TELNET.OPT_EOR ||
172
+ option === TELNET.OPT_BINARY) {
173
+ this.sendTelnet(TELNET.WILL, option);
174
+ }
175
+ else if (option === 0x28) {
176
+ // TN3270E — accept
177
+ this.sendTelnet(TELNET.WILL, option);
178
+ this.tn3270eMode = true;
179
+ }
180
+ else {
181
+ this.sendTelnet(TELNET.WONT, option);
182
+ }
183
+ break;
184
+ case TELNET.WILL:
185
+ if (option === TELNET.OPT_EOR ||
186
+ option === TELNET.OPT_BINARY) {
187
+ this.sendTelnet(TELNET.DO, option);
188
+ }
189
+ else if (option === 0x28) {
190
+ this.sendTelnet(TELNET.DO, option);
191
+ this.tn3270eMode = true;
192
+ }
193
+ else {
194
+ this.sendTelnet(TELNET.DONT, option);
195
+ }
196
+ break;
197
+ case TELNET.DONT:
198
+ this.sendTelnet(TELNET.WONT, option);
199
+ break;
200
+ case TELNET.WONT:
201
+ this.sendTelnet(TELNET.DONT, option);
202
+ break;
203
+ }
204
+ }
205
+ handleSubnegotiation(data) {
206
+ if (data.length === 0)
207
+ return;
208
+ const option = data[0];
209
+ if (option === TELNET.OPT_TTYPE && data.length >= 2 && data[1] === TELNET.TTYPE_SEND) {
210
+ this.sendTerminalType();
211
+ }
212
+ else if (option === 0x28) {
213
+ // TN3270E subnegotiation
214
+ this.handleTN3270ESubneg(data);
215
+ }
216
+ }
217
+ sendTerminalType() {
218
+ const typeStr = TERMINAL_TYPE;
219
+ const buf = Buffer.alloc(4 + typeStr.length + 2);
220
+ let i = 0;
221
+ buf[i++] = TELNET.IAC;
222
+ buf[i++] = TELNET.SB;
223
+ buf[i++] = TELNET.OPT_TTYPE;
224
+ buf[i++] = TELNET.TTYPE_IS;
225
+ for (let j = 0; j < typeStr.length; j++) {
226
+ buf[i++] = typeStr.charCodeAt(j);
227
+ }
228
+ buf[i++] = TELNET.IAC;
229
+ buf[i++] = TELNET.SE;
230
+ this.sendRaw(buf);
231
+ }
232
+ handleTN3270ESubneg(data) {
233
+ if (data.length < 2)
234
+ return;
235
+ const msgType = data[1];
236
+ // TN3270E DEVICE-TYPE SEND (0x08 0x02)
237
+ if (msgType === 0x02) {
238
+ // Send device type response
239
+ const typeStr = TERMINAL_TYPE;
240
+ const resp = Buffer.alloc(4 + typeStr.length + 2);
241
+ let i = 0;
242
+ resp[i++] = TELNET.IAC;
243
+ resp[i++] = TELNET.SB;
244
+ resp[i++] = 0x28; // TN3270E
245
+ resp[i++] = 0x02; // DEVICE-TYPE IS
246
+ for (let j = 0; j < typeStr.length; j++) {
247
+ resp[i++] = typeStr.charCodeAt(j);
248
+ }
249
+ resp[i++] = TELNET.IAC;
250
+ resp[i++] = TELNET.SE;
251
+ this.sendRaw(resp);
252
+ }
253
+ // TN3270E FUNCTIONS REQUEST (0x08 0x04)
254
+ if (msgType === 0x04) {
255
+ // Accept no functions
256
+ this.sendRaw(Buffer.from([
257
+ TELNET.IAC, TELNET.SB, 0x28,
258
+ 0x04, // FUNCTIONS IS
259
+ TELNET.IAC, TELNET.SE,
260
+ ]));
261
+ }
262
+ }
263
+ sendTelnet(cmd, option) {
264
+ this.sendRaw(Buffer.from([TELNET.IAC, cmd, option]));
265
+ }
266
+ }