695 lines
20 KiB
Go
695 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
|
|
|
|
// SupportsGE returns true if the client indicated it supports graphic
|
|
// escape to code page 310.
|
|
SupportsGE() bool
|
|
|
|
// 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 hasGE bool
|
|
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, false}, 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, false}, 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, hasGE = 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, hasGE}, 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. Also returns a boolean
|
|
// indicating if graphic escape to code page 310 is supported. If unable,
|
|
// returns 0. The byte slice passed in to buf must begin with {0x81, 0x85}.
|
|
func getCodepageID(buf []byte) (int, bool) {
|
|
// Initial validity check.
|
|
if len(buf) < 11 || buf[0] != 0x81 || buf[1] != 0x85 {
|
|
return 0, false
|
|
}
|
|
|
|
// If the GF bit is not set, no point in continuing.
|
|
if buf[2]&(1<<1) != 1<<1 {
|
|
return 0, false
|
|
}
|
|
|
|
// ALT flag = graphic escape supported
|
|
hasGE := buf[2]&(1<<7) == 1<<7
|
|
|
|
// Descriptor length
|
|
dl := int(buf[10])
|
|
|
|
// There may be more than one descriptors, and we need to find local ID 0,
|
|
// and possibly local ID 1 (the alternate codepage).
|
|
var primaryCPID, altCPID int
|
|
pos := 11 // first descriptor
|
|
for {
|
|
if len(buf) < pos+dl {
|
|
// No more descriptors
|
|
break
|
|
}
|
|
|
|
// CPID of this descriptor. No matter how long the descriptor is, the
|
|
// code page will be 2-byte big endian integer in the last two bytes.
|
|
cpid := int(buf[pos+dl-2])<<8 + int(buf[pos+dl-1])
|
|
|
|
// First byte of the descriptor is the local ID; 0 is the base set and
|
|
// 1 is the alternate (graphic escape) set.
|
|
if buf[pos] == 0 && primaryCPID == 0 {
|
|
primaryCPID = cpid
|
|
} else if buf[pos] == 1 {
|
|
altCPID = cpid
|
|
}
|
|
|
|
pos += dl
|
|
}
|
|
|
|
return primaryCPID, hasGE && altCPID == 310
|
|
}
|
|
|
|
// 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
|
|
hasGE bool
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (d *deviceInfo) SupportsGE() bool {
|
|
return d.hasGE
|
|
}
|