Files
sessman3270/protocol.go
T
2026-10-17 21:12:15 +00:00

431 lines
11 KiB
Go

package main
import (
"bytes"
"fmt"
"net"
"strings"
"time"
"github.com/racingmars/go3270"
)
func indexTelnetEOR(b []byte) int {
for i := 0; i+1 < len(b); i++ {
if b[i] == IAC {
switch b[i+1] {
case IAC:
i++ // skip second 0xFF of escaped IAC IAC pair
case EOR:
return i
}
}
}
return -1
}
func lastIndexTelnetEOR(b []byte) int {
last := -1
for i := 0; i+1 < len(b); i++ {
if b[i] == IAC {
switch b[i+1] {
case IAC:
i++
case EOR:
last = i
i++
}
}
}
return last
}
// negotiate RFC 2355 TN3270E or RFC 1576 TN3270 options with an upstream host
func negotiateUpstreamTelnet(conn net.Conn, termType string) ([]byte, bool, bool, error) {
_ = conn.SetDeadline(time.Now().Add(7 * time.Second))
defer conn.SetDeadline(time.Time{})
var isTN3270E bool
var devTypeSent bool
var functionsDone bool
var agreedBindImage bool
var ourBinary, hisBinary bool
var ourEOR, hisEOR bool
var ttypeSent bool
buf := make([]byte, 8192)
var initial3270Data []byte
for {
isTN3270EDone := isTN3270E && devTypeSent && functionsDone
isTN3270Done := !isTN3270E && ourBinary && hisBinary && ourEOR && hisEOR && ttypeSent
if isTN3270EDone || isTN3270Done || len(initial3270Data) > 0 {
break
}
n, err := conn.Read(buf)
if err != nil {
if (isTN3270E && functionsDone) || (ourBinary && hisBinary && ourEOR && hisEOR) {
break
}
return nil, false, false, fmt.Errorf("read during upstream negotiation: %w", err)
}
data := buf[:n]
var replies []byte
i := 0
for i < len(data) {
if data[i] == IAC && i+1 < len(data) {
cmd := data[i+1]
if cmd == SB {
seIdx := bytes.Index(data[i:], []byte{IAC, SE})
if seIdx != -1 {
sbPayload := data[i+2 : i+seIdx]
if len(sbPayload) >= 2 && sbPayload[0] == OPT_TERMINAL_TYPE && sbPayload[1] == TTYPE_SEND {
replies = append(replies, IAC, SB, OPT_TERMINAL_TYPE, TTYPE_IS)
replies = append(replies, []byte(termType)...)
replies = append(replies, IAC, SE)
ttypeSent = true
} else if len(sbPayload) >= 3 && sbPayload[0] == OPT_TN3270E {
switch sbPayload[1] {
case TN3270E_SEND:
if sbPayload[2] == TN3270E_DEVICE_TYPE {
// RFC 2355: TN3270E device-type must NOT contain -E suffix
devType := strings.TrimSuffix(termType, "-E")
replies = append(replies, IAC, SB, OPT_TN3270E, TN3270E_DEVICE_TYPE, TN3270E_REQUEST)
replies = append(replies, []byte(devType)...)
replies = append(replies, IAC, SE)
devTypeSent = true
}
case TN3270E_DEVICE_TYPE:
switch sbPayload[2] {
case TN3270E_IS:
replies = append(replies, IAC, SB, OPT_TN3270E, TN3270E_FUNCTIONS, TN3270E_REQUEST, TN3270E_FUNC_RESPONSES, TN3270E_FUNC_BIND_IMAGE, IAC, SE)
case TN3270E_REJECT:
// Server rejected TN3270E device type -> fall back to basic TN3270
isTN3270E = false
replies = append(replies, IAC, WONT, OPT_TN3270E, IAC, DONT, OPT_TN3270E)
}
case TN3270E_FUNCTIONS:
if sbPayload[2] == TN3270E_REQUEST || sbPayload[2] == TN3270E_IS {
funcs := sbPayload[3:]
if bytes.Contains(funcs, []byte{TN3270E_FUNC_BIND_IMAGE}) {
agreedBindImage = true
}
if sbPayload[2] == TN3270E_REQUEST {
replies = append(replies, IAC, SB, OPT_TN3270E, TN3270E_FUNCTIONS, TN3270E_IS)
replies = append(replies, funcs...)
replies = append(replies, IAC, SE)
}
functionsDone = true
}
}
}
i += seIdx + 2
continue
}
}
if i+2 < len(data) {
opt := data[i+2]
switch cmd {
case DO:
switch opt {
case OPT_TN3270E:
replies = append(replies, IAC, WILL, opt)
isTN3270E = true
case OPT_BINARY:
replies = append(replies, IAC, WILL, opt)
ourBinary = true
case OPT_EOR:
replies = append(replies, IAC, WILL, opt)
ourEOR = true
case OPT_TIME_MARK, OPT_SUPPRESS_GA, OPT_TERMINAL_TYPE:
replies = append(replies, IAC, WILL, opt)
default:
replies = append(replies, IAC, WONT, opt)
}
i += 3
continue
case WILL:
switch opt {
case OPT_TN3270E:
replies = append(replies, IAC, DO, opt)
isTN3270E = true
case OPT_BINARY:
replies = append(replies, IAC, DO, opt)
hisBinary = true
case OPT_EOR:
replies = append(replies, IAC, DO, opt)
hisEOR = true
case OPT_SUPPRESS_GA, OPT_TERMINAL_TYPE:
replies = append(replies, IAC, DO, opt)
default:
replies = append(replies, IAC, DONT, opt)
}
i += 3
continue
case DONT:
switch opt {
case OPT_TN3270E:
isTN3270E = false
case OPT_BINARY:
ourBinary = false
case OPT_EOR:
ourEOR = false
}
replies = append(replies, IAC, WONT, opt)
i += 3
continue
case WONT:
switch opt {
case OPT_TN3270E:
isTN3270E = false
case OPT_BINARY:
hisBinary = false
case OPT_EOR:
hisEOR = false
}
replies = append(replies, IAC, DONT, opt)
i += 3
continue
}
}
// 2-byte Telnet commands like NOP (0xF1), GA (0xF9)
if cmd >= NOP && cmd <= GA {
i += 2
continue
}
}
// Non-negotiation bytes (initial 3270 data stream)
initial3270Data = append(initial3270Data, data[i:]...)
break
}
if len(replies) > 0 {
if _, err := conn.Write(replies); err != nil {
return nil, false, false, err
}
}
}
finalTN3270E := isTN3270E && functionsDone
var isBound bool
if finalTN3270E && !agreedBindImage {
isBound = true
}
// If TN3270E, parse initial3270Data records
if finalTN3270E && len(initial3270Data) >= 5 {
var displayable []byte
recBuf := initial3270Data
for len(recBuf) >= 5 {
eorIdx := indexTelnetEOR(recBuf)
var record []byte
if eorIdx != -1 {
record = recBuf[:eorIdx+2]
recBuf = recBuf[eorIdx+2:]
} else {
record = recBuf
recBuf = nil
}
if len(record) >= 5 {
dataType := record[0]
switch dataType {
case TN3270E_DATA_BIND:
isBound = true
case TN3270E_DATA_UNBIND:
isBound = false
case TN3270E_DATA_3270, TN3270E_DATA_SSCP_LU:
isBound = (dataType == TN3270E_DATA_3270)
displayable = append(displayable, record[5:]...)
if eorIdx == -1 && len(record) > 5 {
displayable = append(displayable, IAC, EOR)
}
}
}
}
initial3270Data = displayable
}
return initial3270Data, finalTN3270E, isBound, nil
}
func extractAID(packet []byte) go3270.AID {
for i := 0; i < len(packet); {
if packet[i] == IAC {
if i+1 < len(packet) {
switch packet[i+1] {
case DO, DONT, WILL, WONT:
i += 3
continue
case SB:
seIdx := bytes.Index(packet[i:], []byte{IAC, SE})
if seIdx != -1 {
i += seIdx + 2
continue
}
}
}
i += 2
continue
}
return go3270.AID(packet[i])
}
return go3270.AIDNone
}
func is3270Query(data []byte) bool {
if len(data) < 3 {
return false
}
idx := bytes.IndexByte(data, CMD_WSF)
if idx == -1 {
return false
}
return bytes.Contains(data[idx+1:], []byte{SF_READ_PARTITION, SF_RP_QUERY})
}
func buildQueryReply(rows, cols int, isColor bool, isTN3270E bool, isBound bool) []byte {
var reply []byte
// 1. AID byte
reply = append(reply, AID_QUERY_REPLY)
// 2. Structured Field: Usable Area (0x81 0x81)
cells := rows * cols
usableArea := []byte{
0x00, 0x1A, // Length = 26
SF_QUERY_REPLY, QR_USABLE_AREA, // SFID = Usable Area
0x00, 0x00, // Flags (12/14-bit addressing)
byte(cols >> 8), byte(cols & 0xFF), // Width
byte(rows >> 8), byte(rows & 0xFF), // Height
0x01, // Units = inches (0.01 in)
0x00, 0x0A, 0x00, 0x0A, // Xr, Yr
0x00, 0x0A, 0x00, 0x0A, // Aw, Ah
byte(cells >> 8), byte(cells & 0xFF), // Buffer size
}
reply = append(reply, usableArea...)
// 3. Structured Field: Color (0x81 0x86) if color
if isColor {
colorSF := []byte{
0x00, 0x12, // Length = 18
SF_QUERY_REPLY, QR_COLOR, // SFID = Color
0x00, 0x00, // Flags
0x00, // Default color
0x00, // Neutral color
ATTR_COLOR_BLUE, 0x00, // Blue
ATTR_COLOR_RED, 0x00, // Red
ATTR_COLOR_PINK, 0x00, // Pink
ATTR_COLOR_GREEN, 0x00, // Green
ATTR_COLOR_TURQUOISE, 0x00, // Turquoise
ATTR_COLOR_YELLOW, 0x00, // Yellow
ATTR_COLOR_WHITE, 0x00, // Neutral/White
}
reply = append(reply, colorSF...)
}
// 4. Structured Field: Highlight (0x81 0x87)
highlightSF := []byte{
0x00, 0x0D, // Length = 13
SF_QUERY_REPLY, QR_HIGHLIGHTING, // SFID = Highlight
0x00, 0x00, // Flags
0x00, 0x00, // Default
ATTR_HL_BLINK, 0x00, // Blink
ATTR_HL_REVERSE, 0x00, // Reverse Video
ATTR_HL_UNDERSCORE, 0x00, // Underscore
}
reply = append(reply, highlightSF...)
// 5. Structured Field: Implicit Partition (0x81 0xA6)
implicitSF := []byte{
0x00, 0x08, // Length = 8
SF_QUERY_REPLY, QR_IMPLICIT_PARTITION, // SFID = Implicit Partition
0x00, 0x00, // Flags
byte(cols >> 8), byte(cols & 0xFF), // Alternate width
byte(rows >> 8), byte(rows & 0xFF), // Alternate height
}
reply = append(reply, implicitSF...)
// 6. Structured Field: Null / End of Query List
nullSF := []byte{
0x00, 0x04,
SF_QUERY_REPLY, QR_NULL,
}
reply = append(reply, nullSF...)
headerLen := 0
if isTN3270E {
headerLen = 5
}
escaped := make([]byte, headerLen, len(reply)*2+headerLen+2)
for _, b := range reply {
if b == IAC {
escaped = append(escaped, IAC, IAC)
} else {
escaped = append(escaped, b)
}
}
escaped = append(escaped, IAC, EOR)
if isTN3270E {
dataType := TN3270E_DATA_3270
if !isBound {
dataType = TN3270E_DATA_SSCP_LU
}
escaped[0] = dataType
escaped[1] = 0x00
escaped[2] = 0x00
escaped[3] = 0x00
escaped[4] = 0x00
}
return escaped
}
func GetTerminalMatrix(termType string, devInfo go3270.DevInfo) (rows, cols, maxBuf int) {
if devInfo != nil {
rows, cols = devInfo.AltDimensions()
}
if rows <= 0 || cols <= 0 {
rows, cols = 24, 80
for _, p := range strings.Split(strings.TrimSuffix(termType, ")"), "-") {
switch p {
case "3":
rows, cols = 32, 80
case "4":
rows, cols = 43, 80
case "5":
rows, cols = 27, 132
}
}
}
maxBuf = min(max(rows*cols*4, 8192), 65536)
return rows, cols, maxBuf
}
func ResolveTerminalType(origTermType string, devInfo go3270.DevInfo) string {
rows, cols, _ := GetTerminalMatrix(origTermType, devInfo)
model := 2
switch {
case rows == 32 && cols == 80:
model = 3
case rows == 43 && cols == 80:
model = 4
case rows == 27 && cols == 132:
model = 5
}
family := "3279"
if strings.Contains(strings.ToUpper(origTermType), "3278") {
family = "3278"
}
return fmt.Sprintf("IBM-%s-%d-E", family, model)
}