From adb42dea54d485d2be33594e8eab0a2f9c0b0f75 Mon Sep 17 00:00:00 2001 From: "Matthew R. Wilson" Date: Mon, 2 Nov 2020 16:12:45 -0800 Subject: [PATCH] Early response parsing work --- bufaddr_test.go | 33 ++++++++ example/example.go | 28 +++---- response.go | 192 +++++++++++++++++++++++++++++++++++++++++++++ screen.go | 91 ++++++++++++++++----- util.go | 46 +++++++++++ 5 files changed, 354 insertions(+), 36 deletions(-) create mode 100644 bufaddr_test.go create mode 100644 response.go create mode 100644 util.go diff --git a/bufaddr_test.go b/bufaddr_test.go new file mode 100644 index 0000000..f296087 --- /dev/null +++ b/bufaddr_test.go @@ -0,0 +1,33 @@ +// This file is part of https://github.com/racingmars/go3270/ +// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See +// LICENSE in the project root for license information. + +package go3270 + +import ( + "testing" +) + +func TestEncode(t *testing.T) { + encoded := getpos(0, 0) + if encoded[0] != 0x40 || encoded[1] != 0x40 { + t.Error("Position (0, 0) not correctly encoded") + } + + encoded = getpos(11, 39) + if encoded[0] != 0x4e || encoded[1] != 0xd7 { + t.Error("Position (11, 39) not correctly encoded") + } +} + +func TestDecode(t *testing.T) { + decoded := decodeBufAddr([2]byte{0x40, 0x40}) + if decoded != 0 { + t.Error("Buffer address incorrectly decoded") + } + + decoded = decodeBufAddr([2]byte{0x4e, 0xd7}) + if decoded != 919 { + t.Error("Buffer address incorrectly decoded") + } +} diff --git a/example/example.go b/example/example.go index bef1218..7575268 100644 --- a/example/example.go +++ b/example/example.go @@ -7,14 +7,22 @@ package main import ( "fmt" "net" + "os" "github.com/racingmars/go3270" ) +func init() { + go3270.Debug = os.Stderr +} + +// A Screen is an array of go3270.Field structs: var loginScreen = go3270.Screen{ - {Row: 0, Col: 0, Intense: true, Content: "Testing . . ."}, - {Row: 1, Col: 0, Content: "Name . . ."}, - {Row: 1, Col: 14, Name: "name", Write: true}, + {Row: 0, Col: 30, Intense: true, Content: "3270 Example Screen"}, + {Row: 1, Col: 0, Content: "First Name . . ."}, + {Row: 1, Col: 18, Name: "fname", Write: true, Content: "Test"}, + {Row: 2, Col: 0, Content: "Last Name . . . ."}, + {Row: 2, Col: 18, Name: "lname", Write: true}, } func main() { @@ -35,21 +43,11 @@ func handle(conn net.Conn) { defer conn.Close() go3270.NegotiateTelnet(conn) - err := go3270.WriteScreen(loginScreen, 10, 15, conn) + _, err := go3270.ShowScreen(loginScreen, nil, 1, 19, conn) if err != nil { panic(err) } - for { - rbuf := make([]byte, 255) - n, err := conn.Read(rbuf) - if err != nil { - break - } - for i := 0; i < n; i++ { - fmt.Printf("%x", rbuf[i]) - } - fmt.Printf("\n") - } + fmt.Println("Connection closed") } diff --git a/response.go b/response.go new file mode 100644 index 0000000..e78b0d4 --- /dev/null +++ b/response.go @@ -0,0 +1,192 @@ +// This file is part of https://github.com/racingmars/go3270/ +// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See +// LICENSE in the project root for license information. + +package go3270 + +import ( + "bytes" + "net" +) + +// Response encapsulates data received from a 3270 client in response to the +// previously sent screen. +type Response struct { + // Which Action ID key did the user press? + AID AID + + // Row the cursor was on (0-based). + Row int + + // Column the cursor was on (0-based). + Col int + + // Field values. + Values map[string]string +} + +// AID is an Action ID character. +type AID byte + +const ( + AIDNone AID = 0x60 + AIDEnter AID = 0x7D + AIDPF1 AID = 0xF1 + AIDPF2 AID = 0xF2 + AIDPF3 AID = 0xF3 + AIDPF4 AID = 0xF4 + AIDPF5 AID = 0xF5 + AIDPF6 AID = 0xF6 + AIDPF7 AID = 0xF7 + AIDPF8 AID = 0xF8 + AIDPF9 AID = 0xF9 + AIDPF10 AID = 0x7A + AIDPF11 AID = 0x7B + AIDPF12 AID = 0x7C + AIDPF13 AID = 0xC1 + AIDPF14 AID = 0xC2 + AIDPF15 AID = 0xC3 + AIDPF16 AID = 0xC4 + AIDPF17 AID = 0xC5 + AIDPF18 AID = 0xC6 + AIDPF19 AID = 0xC7 + AIDPF20 AID = 0xC8 + AIDPF21 AID = 0xC9 + AIDPF22 AID = 0x4A + AIDPF23 AID = 0x4B + AIDPF24 AID = 0x4C + AIDPA1 AID = 0x6C + AIDPA2 AID = 0x6E + AIDPA3 AID = 0x6B + AIDClear AID = 0x6D +) + +func readResponse(c net.Conn) (Response, error) { + var r Response + aid, err := readAID(c) + if err != nil { + return r, err + } + r.AID = aid + + row, col, _, err := readPosition(c) + if err != nil { + return r, err + } + r.Col = col + r.Row = row + + if err = readFields(c); err != nil { + return r, err + } + + return r, nil +} + +func readAID(c net.Conn) (AID, error) { + buf := make([]byte, 1) + for { + _, err := c.Read(buf) + if err != nil { + return AIDNone, err + } + b := buf[0] + if (b == 0x60) || (b >= 0x6b && b <= 0x6e) || + (b >= 0x7a && b <= 0x7d) || (b >= 0x4a && b <= 0x4c) || + (b >= 0xf1 && b <= 0xf9) || (b >= 0xc1 && b <= 0xc9) { + // We found a valid AID + debugf("Got AID byte: %x\n", b) + return AID(b), nil + } + // Consume non-AID bytes continuing loop + debugf("Got non-AID byte: %x\n", b) + } +} + +func readPosition(c net.Conn) (row, col, addr int, err error) { + buf := make([]byte, 1) + raw := make([]byte, 2) + + // Read two bytes + for i := 0; i < 2; i++ { + if _, err := c.Read(buf); err != nil { + return 0, 0, 0, err + } + raw[i] = buf[0] + } + + // Decode the raw position + addr = decodeBufAddr([2]byte{raw[0], raw[1]}) + row = addr % 80 + col = (addr - row) / 80 + + debugf("Got position bytes %02x %02x, decoded to %d\n", raw[0], raw[1], + addr) + + return row, col, addr, nil +} + +func readFields(c net.Conn) error { + buf := make([]byte, 1) + var infield bool + var fieldpos int + var fieldval bytes.Buffer + var err error + + // consume bytes until we get 0xffef + for { + // Read a byte + if _, err = c.Read(buf); err != nil { + return err + } + + // Check for end of data stream (0xffef) + if buf[0] == 0xff { + // Finish the current field + if infield { + // TODO + debugf("Field %d: %s\n", fieldpos, e2a(fieldval.Bytes())) + } + + // consume the next byte, which is probably 0xef + if _, err = c.Read(buf); err != nil { + return err + } + return nil + } + + // No? Check for start-of-field + if buf[0] == 0x11 { + // Finish the previous field, if necessary + if infield { + // TODO + debugf("Field %d: %s\n", fieldpos, e2a(fieldval.Bytes())) + } + // Start a new field + infield = true + fieldval = bytes.Buffer{} + fieldpos = 0 + + if _, _, fieldpos, err = readPosition(c); err != nil { + return err + } + continue + } + + // Consume all other bytes as field contents if we're in a field + if !infield { + debugf("Got unexpected byte while processing fields: %02x\n", buf[0]) + continue + } + fieldval.WriteByte(buf[0]) + + } +} + +// decodeBufAddr decodes a raw 2-byte encoded buffer address and returns the +// integer value of the address (i.e. 0-1919) +func decodeBufAddr(raw [2]byte) int { + hi := decodes[raw[0]] << 6 + lo := decodes[raw[1]] + return hi | lo +} diff --git a/screen.go b/screen.go index cb39f08..36fce45 100644 --- a/screen.go +++ b/screen.go @@ -7,7 +7,7 @@ package go3270 import ( "bytes" "fmt" - "io" + "net" ) // Field is a field on the 3270 screen. @@ -41,13 +41,19 @@ type Field struct { // names, type Screen []Field -// WriteScreen writes the 3270 datastream for the screen to a writer. Fields -// that aren't valid (e.g. outside of the 24x80 screen) are silently ignored. -// After writing the fields, the curser is set to crow, ccol, which are -// 0-based positions: row 0-23 and col 0-79. Errors from io.Writer.Write() -// are returned if encountered. -func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error { +// ShowScreen writes the 3270 datastream for the screen to a connection. +// Fields that aren't valid (e.g. outside of the 24x80 screen) are silently +// ignored. If a named field has an entry in the values map, the content of +// the field from the values map is used INSTEAD OF the Field struct's Content +// field. The values map may be nil if no overrides are needed. After writing +// the fields, the cursor is set to crow, ccol, which are 0-based positions: +// row 0-23 and col 0-79. Errors from conn.Write() are returned if +// encountered. +func ShowScreen(screen Screen, values map[string]string, crow, ccol int, + conn net.Conn) (Response, error) { + var b bytes.Buffer + var fieldmap = make(map[int]string) // field buffer positions -> name b.WriteByte(0xf5) // Erase/Write to terminal b.WriteByte(0xc3) // WCC = Reset, Unlock Keyboard, Reset MDT @@ -61,8 +67,23 @@ func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error { b.Write(sba(fld.Row, fld.Col)) b.Write(sf(fld.Write, fld.Intense)) - if fld.Content != "" { - b.Write(a2e([]byte(fld.Content))) + + // Use fld.Content, unless the field is named and appears in the + // value map. + content := fld.Content + if fld.Name != "" { + if val, ok := values[fld.Name]; ok { + content = val + } + } + if content != "" { + b.Write(a2e([]byte(content))) + } + + // If a writable field, add it to the field map + if fld.Write { + bufaddr := fld.Row*80 + fld.Col + fieldmap[bufaddr] = fld.Name } } @@ -73,14 +94,33 @@ func WriteScreen(screen Screen, crow, ccol int, w io.Writer) error { if ccol < 0 || ccol > 79 { ccol = 0 } - //b.Write(ic(crow, ccol)) + b.Write(ic(crow, ccol)) b.Write([]byte{0xff, 0xef}) // Telnet IAC EOR // Now write the datastream to the writer, returning any potential error. - fmt.Printf("%x\n", b.Bytes()) - _, err := w.Write(b.Bytes()) - return err + if Debug != nil { + fmt.Fprintf(Debug, "%x\n", b.Bytes()) + } + if _, err := conn.Write(b.Bytes()); err != nil { + return Response{}, err + } + + // Now wait for the response. We want to read bytes that start with an AID + // and end with 0xFFEF. + // for { + // rbuf := make([]byte, 1) + // n, err := conn.Read(rbuf) + // if err != nil { + // return Response{}, err + // } + // for i := 0; i < n; i++ { + // fmt.Printf("%x", rbuf[i]) + // } + // fmt.Printf("\n") + // } + + return readResponse(conn) } // sba is the "set buffer address" 3270 command. @@ -97,6 +137,11 @@ func sf(write, intense bool) []byte { result[0] = 0x1d // SF if !write { result[1] |= 1 << 5 // set "bit 2" + } else { + // The MDT bit -- we always want writable field values returned, + // even if unchanged + result[1] |= 1 // set "bit 7" + } if intense { result[1] |= 1 << 3 // set "bit 4" @@ -126,11 +171,15 @@ func getpos(row, col int) []byte { return result } -// codes are the 3270 control character I/O codes, pre-computed as provided -// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm -var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, - 0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4, - 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, - 0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0x6a, 0x6b, 0x6c, - 0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, - 0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f} +type readerState int + +const ( + stateNone readerState = iota + stateGotAID + stateGotFirstAddr + stateGotSecondAddr + stateInField + stateGotFirstFieldAddr + stateGotSecondFieldAddr + stateGot +) diff --git a/util.go b/util.go new file mode 100644 index 0000000..aa92c96 --- /dev/null +++ b/util.go @@ -0,0 +1,46 @@ +// This file is part of https://github.com/racingmars/go3270/ +// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See +// LICENSE in the project root for license information. + +package go3270 + +import ( + "fmt" + "io" +) + +var Debug io.Writer + +func debugf(format string, a ...interface{}) { + if Debug == nil { + return + } + + fmt.Fprintf(Debug, format, a...) +} + +// codes are the 3270 control character I/O codes, pre-computed as provided +// at http://www.tommysprinkle.com/mvs/P3270/iocodes.htm +var codes = []byte{0x40, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, + 0xc9, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0xd1, 0xd2, 0xd3, 0xd4, + 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, + 0x61, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0x6a, 0x6b, 0x6c, + 0x6d, 0x6e, 0x6f, 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f} + +// decodes is the inverse of the above table; -1 is used in invalid positions +var decodes = []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, 16, -1, -1, -1, -1, -1, + -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, -1, -1, -1, -1, -1, -1, + -1, -1, 42, 43, 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 58, 59, 60, 61, 62, 63, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 2, + 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, -1, 17, 18, 19, 20, 21, 22, + 23, 24, 25, -1, -1, -1, -1, -1, -1, -1, -1, 34, 35, 36, 37, 38, 39, 40, + 41, -1, -1, -1, -1, -1, -1, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, + -1, -1, -1, -1}