Files
go3270/telnet.go
T

676 lines
20 KiB
Go

// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020, 2025 by Matthew R. Wilson, licensed under the MIT license.
// See LICENSE in the project root for license information.
package go3270
import (
"errors"
"fmt"
"net"
"os"
"regexp"
"time"
)
// DevInfo provides information about the terminal that is connected.
//
// 3270 terminals operate at a default screen size of 24 rows that are 80
// columns wide. The normal "Write/Erase" datastream command always writes to
// the default 24x80 buffer. But some terminals support more rows and/or
// columns, and the alternate sized buffer may be written to with the
// "Write/Erase Alternate" command.
type DevInfo interface {
// AltDimensions returns the number or rows and columns on the alternate
// screen size.
AltDimensions() (rows, cols int)
// TerminalType reports the terminal-provided identification string. All
// modern tn3270 clients will report one of the IBM-3278 models (-2, -3,
// -4, or -5), or IBM-DYNAMIC if the alternate screen size isn't one of
// the fixed sizes of the 3278 models. This string is purely
// informational; the actual size of the alternate screen is available
// from AltDimensions().
TerminalType() string
// Codepage is the Codepage interface that implements the EBCDIC
// translation for the detected code page for the terminal, if supported.
// This may be nil if the client code page is unknown. Whenever calling
// the screen functions, always pass the value returned by this Codepage()
// function in the ScreenOpts (nil is allowed to default to the global
// default codepage).
Codepage() Codepage
// Private version of AltDimensions() so callers can't fake us out; only
// real implementations returned by NegotiateTelnet() will work.
altDimensions() (rows, cols int)
}
const (
se = 240 // 0xf0
sb = 250 // 0xfa
will = 251 // 0xfb
wont = 252 // 0xfc
do = 253 // 0xfd
dont = 254 // 0xfe
iac = 255 // 0xff
// Options
binaryOption = 0
eorOption = 25 // 0x19
eor = 239 // 0xf1
terminalType = 24 // 0x18
terminalTypeIs = 0
terminalTypeSend = 1
)
// ErrNo3270 indicates that the telnet client did not respond properly to the
// options negotiation that are expected for a tn3270 client.
var ErrNo3270 = errors.New("couldn't negotiate telnet options for tn3270")
// ErrTelnetError indicates an unexpected response was encountered in the
// telnet protocol.
var ErrTelnetError = errors.New("telnet or 3270 protocol error")
// ErrUnknownTerminal indicates the client did not identify itself as an
// IBM-3277, 3278, 3279, or IBM-DYNAMIC model. All modern tn3270 clients
// should report as IBM-3278 models or IBM-DYNAMIC.
var ErrUnknownTerminal = errors.New("unknown terminal type")
var errOptionRejected = errors.New("option rejected")
// NegotiateTelnet will negotiate the options necessary for tn3270 on a new
// telnet connection, conn.
func NegotiateTelnet(conn net.Conn) (DevInfo, error) {
// Sometimes the client will trigger us to send our "will" assertions
// sooner than we otherwise would. Keep track here so we know not to send
// them again.
var sentWillBin, sentWillEOR bool
// Enable terminal type option
if _, err := conn.Write([]byte{iac, do, terminalType}); err != nil {
return nil, err
}
err := checkOptionResponse(conn, terminalType, do,
&sentWillEOR, &sentWillBin)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Switch to the first available terminal type
conn.Write([]byte{iac, sb, terminalType, terminalTypeSend, iac, se})
devtype, err := getTerminalType(conn)
if err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Request end of record mode
conn.Write([]byte{iac, do, eorOption})
err = checkOptionResponse(conn, eorOption, do,
&sentWillEOR, &sentWillBin)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// Request binary mode
conn.Write([]byte{iac, do, binaryOption})
err = checkOptionResponse(conn, binaryOption, do,
&sentWillEOR, &sentWillBin)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
// It's possible there are already some client requests in the queue
// that we haven't processed yet. We'll need to consume any outstanding
// requests here and respond if necessary.
var buf [3]byte
for {
conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
n, err := conn.Read(buf[:])
conn.SetReadDeadline(time.Time{})
if err != nil {
if ne, ok := err.(net.Error); ok && ne.Timeout() {
// No data waiting. We expect to eventually break out of the
// for loop here.
break
} else {
return nil, err
}
} else if n == 3 {
if buf[0] == iac && buf[1] == do && buf[2] == eorOption {
conn.Write([]byte{iac, will, eorOption})
sentWillEOR = true
} else if buf[0] == iac && buf[1] == do && buf[2] == binaryOption {
conn.Write([]byte{iac, will, binaryOption})
sentWillBin = true
}
} else {
fmt.Println("SHORT READ SHORT READ")
}
}
// Enter end of record mode
if !sentWillEOR {
conn.Write([]byte{iac, will, eorOption})
err = checkOptionResponse(conn, eorOption, will,
&sentWillEOR, &sentWillBin)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
}
// Enter binary mode
if !sentWillBin {
conn.Write([]byte{iac, will, binaryOption})
err = checkOptionResponse(conn, binaryOption, will,
&sentWillEOR, &sentWillBin)
if err == errOptionRejected || err == ErrTelnetError {
return nil, ErrNo3270
} else if err != nil {
return nil, err
}
}
devinfo, err := makeDeviceInfo(conn, devtype)
if err != nil {
return nil, err
}
return devinfo, nil
}
// checkOptionResponse will check for the client's "will/wont" (if mode is do)
// or "do/dont" (if mode is will) response. mode is the option command the
// server just sent, and option is the option code to check for.
//
// If we end up getting a client request instead, we'll response and set
// sentEor or sentBin before trying to read the response again.
func checkOptionResponse(conn net.Conn, option, mode byte,
sentEor, sentBin *bool) error {
var buf [3]byte
var expectedYes, expectedNo byte
switch mode {
case do:
expectedYes = will
expectedNo = wont
case will:
expectedYes = do
expectedNo = dont
default:
return ErrTelnetError
}
n, err := conn.Read(buf[:])
if err != nil {
return err
}
if n < 3 || buf[0] != iac {
return ErrTelnetError
}
// If the client is requesting to negotiate a mode with us before the
// response to our request, we'll satisfy it if it's one of the expected
// modes and then try to read the client's response again.
//
// We only want to do this if we're not already expecting a "do" response
// for the particular option.
if !(expectedYes == do && buf[2] == option) {
if buf[0] == iac && buf[1] == do && buf[2] == eorOption {
conn.Write([]byte{iac, will, eorOption})
*sentEor = true
return checkOptionResponse(conn, option, mode, sentEor, sentBin)
} else if buf[0] == iac && buf[1] == do && buf[2] == binaryOption {
conn.Write([]byte{iac, will, binaryOption})
*sentBin = true
return checkOptionResponse(conn, option, mode, sentEor, sentBin)
}
}
if buf[1] == expectedNo {
// Was the correct option rejected?
if buf[2] != option {
return ErrTelnetError
}
return errOptionRejected
}
if buf[1] != expectedYes {
return ErrTelnetError
}
// We have "will" now. But for the right option?
if buf[2] != option {
return ErrTelnetError
}
// All good, client accepted the option we requested.
return nil
}
// getTerminalType reads the response to a "send terminal type" option
// subfield command.
func getTerminalType(conn net.Conn) (string, error) {
var buf [100]byte
var termtype string
n, err := conn.Read(buf[:])
if err != nil {
return termtype, err
}
// At a minimum, with a one-character terminal type name, we expect
// 7 bytes
if n < 7 {
return termtype, ErrTelnetError
}
// We'll check the expected control bytes all in one go...
if buf[0] != iac || buf[1] != sb || buf[2] != terminalType ||
buf[3] != terminalTypeIs || buf[n-2] != iac || buf[n-1] != se {
return termtype, ErrTelnetError
}
// Everything looks good. The terminal type is an ASCII string between all
// the control/command bytes.
return string(buf[4 : n-2]), nil
}
var modelRegex = regexp.MustCompile(`^IBM-\d{4}-([2-5])`)
func makeDeviceInfo(conn net.Conn, termtype string) (DevInfo, error) {
var rows, cols, cpid int
var codepage Codepage
var isx3270 bool
// tn3270e restricts to a small list of valid models, but since we're
// not doing tn3270e, we are seeing a variety of model numbers. We'll
// generically handle anything claiming to be a -2, -3, -4, or -5 type.
//
// We'll default to known terminal sizes in case we don't get the
// structured field query response later.
modelresult := modelRegex.FindStringSubmatch(termtype)
if len(modelresult) == 2 {
switch modelresult[1] {
case "2":
rows = 24
cols = 80
case "3":
rows = 32
cols = 80
case "4":
rows = 43
cols = 80
case "5":
rows = 27
cols = 132
}
} else if termtype != "IBM-DYNAMIC" {
// If it's not a fixed-size type, it should be IBM-DYNAMIC. If it
// isn't, we don't know how to deal with it. We'll just fall back on a
// simple 24x80 assumption.
rows = 24
cols = 80
termtype = "unknown (" + termtype + ")"
}
// Now we'll discover the terminal size and character set.
// First, we perform an ERASE / WRITE ALTERNATE to clear the screen
// and put it in alternate screen mode. (EWA, reset WCC, telnet EOR)
if _, err := conn.Write([]byte{0x7e, 0xc3, 0xff, 0xef}); err != nil {
return nil, err
}
// Now we need to send the Write Structured Field command (0xf3) with the
// "Read Partition - Query" structured field. Note that we're
// telnet-escaping the 0xff in the data, but the subfield length is the
// *unescaped* length, including the 2 length bytes but excluding the
// telnet EOR (5).
if _, err := conn.Write([]byte{0xf3, 0, 5, 0x01, 0xff, 0xff, 0x02,
0xff, 0xef}); err != nil {
return nil, err
}
// We'll use a timeout in case the client doesn't support/reply to our
// structured field query.
var aid [1]byte
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
n, err := conn.Read(aid[:])
conn.SetReadDeadline(time.Time{})
if err != nil && errors.Is(err, os.ErrDeadlineExceeded) {
// Timeout. In this case, we'll assume it's because the client didn't
// reply to our query command. In that case, we'll return whatever
// we're already assuming.
return &deviceInfo{24, 80, termtype, nil}, nil
} else if err != nil {
return nil, err
}
if n != 1 || aid[0] != byte(aidQueryResponse) {
// the VM telnet client responds with another Do Binary Transmission
// telnet option for some reason... too primitive to know about
// structured field queries? If we're not getting the kind of reply
// we're expecting, we'll return whatever we're already assuming.
flushConnection(conn, 50*time.Millisecond)
return &deviceInfo{24, 80, termtype, nil}, nil
}
// There are an arbitrary number of query reply structured fields. We are
// only interested in the "Usable Area" SFID=0x81 QCODE=0x81 field and
// "Character Sets" QCODE=0x85 field so we'll just consume any others.
// Consume all data until the EOR is received.
for {
// Two bytes are big-endian length.
buf, err := telnetReadN(conn, 2)
if err != nil {
return nil, err
}
if buf == nil {
// EOR. We're out of fields.
break
}
var l int = int(buf[0])<<8 + int(buf[1])
// Field length includes the 2 length bytes
buf, err = telnetReadN(conn, l-2)
if err != nil {
return nil, err
}
if buf == nil {
return nil, ErrTelnetError
}
// Note that because length isn't at the beginning, offsets in buf are
// 2 less than in the 3270 data stream documentation.
if buf[0] == 0x81 && buf[1] == 0x81 {
// Usable Area
rows, cols, err = getUsableArea(buf)
if err != nil {
return nil, err
}
} else if buf[0] == 0x81 && buf[1] == 0x85 {
// Character Sets
cpid = getCodepageID(buf)
} else if buf[0] == 0x81 && buf[1] == 0xA1 {
// RPQ Names. We use this to determine if the client is x3270
// family.
isx3270 = getRPQNames(buf)
} else {
// Not a field we're interested in
continue
}
}
if cpfunc, ok := codepageToFunction[cpid]; ok {
codepage = cpfunc()
// But if x3270 family, assume that this is really the default
// "bracket" codepage, which reports as 37, not true CP37.
if cpid == 37 && isx3270 {
codepage = CodepageBracket()
}
} else {
// nil codepage will be accepted in ScreenOpts to default to the
// global default codepage.
codepage = nil
}
return &deviceInfo{rows, cols, termtype, codepage}, nil
}
// getUsableArea processes the "Query Reply (Usable Area)" response to return
// the rows and columns count of the terminal. The byte slice passed in to buf
// must begin with {0x81, 0x81}.
func getUsableArea(buf []byte) (rows, cols int, err error) {
// A valid Usable Area reply will always include at least 18 (20 with
// length) bytes.
if len(buf) < 18 || buf[0] != 0x81 || buf[1] != 0x81 {
return 0, 0, ErrTelnetError
}
// big-endian two byte values
cols = int(buf[4])<<8 + int(buf[5])
rows = int(buf[6])<<8 + int(buf[7])
if rows == 0 || cols == 0 {
// Got a Usable Area response but the values are 0?
return 0, 0, ErrUnknownTerminal
}
// We support 12- and 14-bit addressing. Using 16-bit addressing
// would require a mode change and the current API design doesn't
// support tracking the state necessary for that.
//
// We'll limit the reported screen size to what fits in 14-bit
// addressing by removing rows if necessary.
for rows*cols >= 1<<14 {
rows--
}
return rows, cols, nil
}
// getCodepageID processes the "Query Reply (Character Sets)" response to
// return the integer code page number if present. If unable, returns 0. The
// byte slice passed in to buf must begin with {0x81, 0x85}.
func getCodepageID(buf []byte) int {
// Initial validity check.
if len(buf) < 11 || buf[0] != 0x81 || buf[1] != 0x85 {
return 0
}
// If the GF bit is not set, no point in continuing.
if buf[2]&(1<<1) != 1<<1 {
return 0
}
// Descriptor length
dl := int(buf[10])
// There may be more than one descriptors, and we need to find the first
// one with local ID 0.
pos := 11 // first descriptor
for {
if len(buf) < pos+dl {
// No more descriptors and we haven't found anything yet
return 0
}
if buf[pos] != 0 {
// not the descriptor we're looking for, try the next one
pos += dl
continue
}
// This is the first descriptor we've seen with ID 0, we'll use it.
// No matter how long the descriptor is, the code page will 2-byte big
// endian integer in the last two bytes.
cpid := int(buf[pos+dl-2])<<8 + int(buf[pos+dl-1])
return cpid
}
}
// getRPGNames checks the "Query Reply (RPQ NAMES)" response to see if the
// client is in the x3270 family. The byte slice passed in to buf must begin
// with {0x81, 0xA1}.
func getRPQNames(buf []byte) bool {
if len(buf) < 16 {
return false
}
// "x3270" in EBCDIC
if buf[11] == 0xa7 && buf[12] == 0xf3 && buf[13] == 0xf2 &&
buf[14] == 0xf7 && buf[15] == 0xf0 {
return true
}
return false
}
// UnNegotiateTelnet will naively (e.g. not checking client responses) attempt
// to restore the telnet options state to what it was before NegotiateTelnet()
// was called.
func UnNegotiateTelnet(conn net.Conn, timeout time.Duration) error {
conn.Write([]byte{iac, wont, eorOption, iac, wont, binaryOption})
conn.Write([]byte{iac, dont, binaryOption})
conn.Write([]byte{iac, dont, eorOption})
conn.Write([]byte{iac, dont, terminalType})
flushConnection(conn, timeout)
return nil
}
// flushConnection discards all bytes that it can read from conn, allowing up
// to the duration timeout for the first byte to be read.
func flushConnection(conn net.Conn, timeout time.Duration) error {
defer conn.SetReadDeadline(time.Time{})
buffer := make([]byte, 1024)
for {
conn.SetReadDeadline(time.Now().Add(timeout))
n, err := conn.Read(buffer)
if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
debugf("nothing to flush\n")
return nil
}
if err != nil {
debugf("error while flushing: %v\n", err)
return err
}
debugf("%d bytes read while flushing connection\n", n)
// for follow-up reads, reduce the timeout
timeout = time.Second / 2
}
}
// telnetRead returns the next byte of data from the connection c, but
// filters out all telnet commands. If passEOR is true, then telnetRead will
// return upon encountering the telnet End of Record command, setting isEor to
// true. When isEor is true, the value of b is meaningless and must be ignored
// (valid will be false). When valid is true, the value in byte b is a real
// value read from the connection; when value is false, do not use the value
// in b. (For example, a valid byte AND error can be returned in the same
// call.)
func telnetRead(c net.Conn, passEOR bool) (b byte, valid, isEor bool, err error) {
const (
normal = iota
command
subneg
)
buf := make([]byte, 1)
state := normal
for {
bn, berr := c.Read(buf)
// When there are no bytes to process and we received an error, we
// are done no matter what state we're in. Any non-command bytes will
// already be in p, so we return.
if bn == 0 && berr != nil {
return 0, false, false, berr
}
// If we received 0 bytes but no error, we'll just read again.
if bn == 0 {
continue
}
// We got a byte! Let's progress through our state machine.
switch state {
case normal:
if buf[0] == iac {
state = command
debugf("entering telnet command state\n")
} else {
return buf[0], true, false, berr
}
case command:
if buf[0] == 0xff {
debugf("leaving telnet command state; was an escaped 0xff\n")
return 0xff, true, false, nil
} else if buf[0] == sb {
state = subneg
debugf("entering telnet command subnegotiation state\n")
} else if passEOR && buf[0] == eor {
debugf("leaving telnet command state; returning EOR\n")
return 0, false, true, nil
} else {
state = normal
debugf("leaving telnet command state; command was %02x\n",
buf[0])
}
case subneg:
if buf[0] == se {
state = normal
debugf("leaving telnet command subnegotiation state\n")
} else {
// remain in subnegotiation consuming bytes until we get se
debugf("consumed telnet subnegotiation byte: %02x\n", buf[0])
}
}
}
}
// telnetReadN reads n unescaped, valid, non-EOR characters. The returned byte
// slice will always be length n (see special case below, though), unless
// error is non-nil, in which case the byte slice will be nil. Invalid or
// early EOR will return ErrTelnetError.
//
// AS A SPECIAL CASE, if the first byte read is EOR, then the returned byte
// slice AND error will be nil.
func telnetReadN(conn net.Conn, n int) ([]byte, error) {
buf := make([]byte, n)
for i := 0; i < n; i++ {
b, valid, isEor, err := telnetRead(conn, true)
if err != nil {
return nil, err
}
if i == 0 && isEor {
// If we're still on the first byte and it's EOR, return a
// non-error nil value.
return nil, nil
}
if !valid || isEor {
return nil, ErrTelnetError
}
buf[i] = b
}
return buf, nil
}
type deviceInfo struct {
rows, cols int
termtype string
codepage Codepage
}
func (d *deviceInfo) AltDimensions() (rows, cols int) {
return d.rows, d.cols
}
func (d *deviceInfo) TerminalType() string {
return d.termtype
}
func (d *deviceInfo) altDimensions() (rows, cols int) {
return d.rows, d.cols
}
func (d *deviceInfo) Codepage() Codepage {
return d.codepage
}