431 lines
11 KiB
Go
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)
|
|
}
|