initial commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
*.pem
|
||||
*.toml
|
||||
sessman3270
|
||||
@@ -0,0 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <https://unlicense.org/>
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
# Sessman 3270
|
||||
|
||||
A simple session manager for 3270 systems extending go3270
|
||||
|
||||
Written by a human with AI assistance as an attempt to see what hybrid development could consist of and produce. There is a default user in the example config with the same username as password.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Persistent 3270 sessions originating from the sessman host
|
||||
- Ability to reconnect to persistent session from another 3270
|
||||
- PA3 hotkey to background the current session and return to the menu
|
||||
- Multi user by design, public or password based authentication
|
||||
- Line Commands to (H)old, (R)esume, (K)ill or start a (B)ackgrounded session
|
||||
- Admin commands to kill sessions or shut down server remotely
|
||||
- Cross platform, runs anywhere Go is supported
|
||||
|
||||
|
||||
## Deployment
|
||||
|
||||
This project uses native Go tools, to generate a build run
|
||||
|
||||
```bash
|
||||
go build
|
||||
```
|
||||
Installing sessman3270 consists of copying the config.toml.dist file and changing it to fit your needs.
|
||||
- If you wish to use SSL/TLS this is set by command line flags
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
Usage of ./sessman3270:
|
||||
-P Allow public access
|
||||
-c string
|
||||
Path to toml config file (default "config.toml")
|
||||
-k string
|
||||
Path to TLS private key file (PEM)
|
||||
-l string
|
||||
Listen address (default "0.0.0.0:2323")
|
||||
-p string
|
||||
Generate bcrypt hash for password and exit
|
||||
-public
|
||||
Allow public access
|
||||
-t string
|
||||
Path to TLS certificate file (PEM)
|
||||
|
||||
```
|
||||
|
||||
The most basic way to run sessman3270 is to start the executable, it will look for a config.toml or exit.
|
||||
|
||||
```bash
|
||||
./sessman3270
|
||||
```
|
||||
|
||||
Override the listen host with `-l`
|
||||
```bash
|
||||
./sessman3270 -l 192.168.1.20:623
|
||||
```
|
||||
|
||||
Generate a password hash with `-p`
|
||||
|
||||
```bash
|
||||
./sessman3270 -p $mySecretP4ssw0rd
|
||||
```
|
||||
|
||||
Enable TLS by providing a key and certificate
|
||||
|
||||
```bash
|
||||
./sessman3270 -c config.toml -k privkey.pem -t fullchain.pem
|
||||
```
|
||||
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [Go3270](https://https://github.com/racingmars/go3270)
|
||||
- [Proxy3270](https://github.com/racingmars/proxy3270)
|
||||
- [TUBES for z/OS](https://www.macro4.com/products/tubes-zos/)
|
||||
- [Qwen3.6-35B-A3B-MTP-GGUF](https://huggingface.co/byteshape/Qwen3.6-35B-A3B-MTP-GGUF)
|
||||
- [Gemini 3.8 Flash](https://blog.google/innovation-and-ai/models-and-research/gemini-models/3-8-flash-and-3-8-flash-cyber/)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
[server]
|
||||
bind_address = "0.0.0.0:2323"
|
||||
codepage = "037"
|
||||
public = true
|
||||
|
||||
[login]
|
||||
title = "SESSION BROKER"
|
||||
subtitle = "Enter your Username and Password to begin or resume a session."
|
||||
prompt_username = "Username ===>"
|
||||
prompt_password = "Password ===>"
|
||||
instructions = "[Enter] Log on [Clear] Reset"
|
||||
|
||||
[menu]
|
||||
title = "SESSION MANAGER"
|
||||
prompt_select = "Selection ===>"
|
||||
footer = "[1-N] Connect [PA3] Return to Menu [PF3] Disconnect"
|
||||
|
||||
[[systems]]
|
||||
name = "PUBVM"
|
||||
description = "Public VM/SP 6 Service"
|
||||
host_port = "pubvm.org:623"
|
||||
tls = true
|
||||
insecure_skip_verify = true
|
||||
|
||||
[[systems]]
|
||||
name = "HFVMVS"
|
||||
description = "HugFreeVikings MVS"
|
||||
host_port = "MVS.HUGFREEVIKINGS.WTF:1023"
|
||||
tls = false
|
||||
|
||||
[[systems]]
|
||||
name = "Forum3270"
|
||||
description = "3270bbs hosted by Moshix"
|
||||
host_port = "www.moshix.tech:2023"
|
||||
tls = true
|
||||
insecure_skip_verify = true
|
||||
|
||||
[[systems]]
|
||||
name = "z/Xplore"
|
||||
description = "IBM z/Xplore System"
|
||||
host_port = "204.90.115.200:623"
|
||||
tls = false
|
||||
|
||||
[[users]]
|
||||
username = "ibmuser"
|
||||
hash = "$2a$10$/ajS78q3gMgbfiVWkvS5kO/T8tx2yIXuyslg.TeRPgSwyDQgELp9C"
|
||||
admin = true
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module sessman3270
|
||||
|
||||
go 1.27.2
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.6.0
|
||||
github.com/racingmars/go3270 v0.9.13
|
||||
golang.org/x/crypto v0.58.0
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/racingmars/go3270 v0.9.13 h1:qFjzx6zUMR0u1dFN3Wv17D5sXdX/PUqBTt77/4angZk=
|
||||
github.com/racingmars/go3270 v0.9.13/go.mod h1:JCzKbsCGdevsd+2iLMRw3Cd+Wk7vmBeGlnfHmeJEcsU=
|
||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
+3637
File diff suppressed because it is too large
Load Diff
+430
@@ -0,0 +1,430 @@
|
||||
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)
|
||||
}
|
||||
+481
@@ -0,0 +1,481 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/racingmars/go3270"
|
||||
)
|
||||
|
||||
const screenSeparator = "==============================================================================="
|
||||
|
||||
func centerCol(text string) int {
|
||||
return max(0, (80-len(text))/2-1)
|
||||
}
|
||||
|
||||
// send an ephemeral centered status/error notification overlay to the client
|
||||
func send3270Message(conn net.Conn, msg string, cp go3270.Codepage) {
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
col := centerCol(msg)
|
||||
screen := go3270.Screen{
|
||||
{Row: 11, Col: 0, Content: screenSeparator},
|
||||
{Row: 12, Col: col, Content: msg, Intense: true},
|
||||
{Row: 13, Col: 0, Content: screenSeparator},
|
||||
}
|
||||
values := make(map[string]string)
|
||||
_, _ = go3270.ShowScreenOpts(screen, values, conn, go3270.ScreenOpts{
|
||||
Codepage: cp,
|
||||
NoResponse: true,
|
||||
CursorRow: 12,
|
||||
CursorCol: col + 1,
|
||||
})
|
||||
}
|
||||
|
||||
func promptLogin(conn net.Conn, devInfo go3270.DevInfo) (LoginCredentials, error) {
|
||||
title := GlobalConfig.Login.Title
|
||||
subtitle := GlobalConfig.Login.Subtitle
|
||||
instructions := GlobalConfig.Login.Instructions
|
||||
promptUser := GlobalConfig.Login.PromptUsername
|
||||
promptPass := GlobalConfig.Login.PromptPassword
|
||||
|
||||
var cp go3270.Codepage
|
||||
if devInfo != nil {
|
||||
cp = devInfo.Codepage()
|
||||
}
|
||||
|
||||
errMsg := ""
|
||||
|
||||
for {
|
||||
screen := go3270.Screen{
|
||||
{Row: 0, Col: 0, Content: screenSeparator},
|
||||
{Row: 1, Col: centerCol(title), Content: title, Intense: true},
|
||||
{Row: 2, Col: centerCol(subtitle), Content: subtitle},
|
||||
{Row: 3, Col: 0, Content: screenSeparator},
|
||||
|
||||
// Username row
|
||||
{Row: 7, Col: 10, Content: promptUser},
|
||||
{Row: 7, Col: 25, Name: "username", Write: true},
|
||||
{Row: 7, Col: 46, Content: ""},
|
||||
|
||||
// Password row
|
||||
{Row: 9, Col: 10, Content: promptPass},
|
||||
{Row: 9, Col: 25, Name: "password", Write: true, Hidden: true},
|
||||
{Row: 9, Col: 46, Content: ""},
|
||||
|
||||
// Footer
|
||||
{Row: 22, Col: 2, Content: instructions},
|
||||
}
|
||||
|
||||
if errMsg != "" {
|
||||
screen = append(screen, go3270.Field{
|
||||
Row: 12,
|
||||
Col: centerCol(errMsg),
|
||||
Content: errMsg,
|
||||
Intense: true,
|
||||
})
|
||||
}
|
||||
|
||||
values := make(map[string]string)
|
||||
resp, err := go3270.ShowScreenOpts(screen, values, conn, go3270.ScreenOpts{
|
||||
Codepage: cp,
|
||||
CursorRow: 7,
|
||||
CursorCol: 26,
|
||||
})
|
||||
if err != nil {
|
||||
return LoginCredentials{}, err
|
||||
}
|
||||
|
||||
if resp.AID == go3270.AIDClear {
|
||||
errMsg = ""
|
||||
continue
|
||||
}
|
||||
|
||||
user := strings.TrimSpace(resp.Values["username"])
|
||||
pass := strings.TrimSpace(resp.Values["password"])
|
||||
|
||||
if pass == "" {
|
||||
errMsg = "Password is required."
|
||||
continue
|
||||
}
|
||||
|
||||
if user == "" {
|
||||
user = "IBMUSER"
|
||||
}
|
||||
|
||||
key := computeSessionKey(user, pass)
|
||||
return LoginCredentials{
|
||||
Username: user,
|
||||
Password: pass,
|
||||
SessionKey: key,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildMainMenuScreen(s *UserMasterSession) go3270.Screen {
|
||||
menuTitle := ""
|
||||
menuFooter := ""
|
||||
promptSelect := ""
|
||||
if GlobalConfig != nil {
|
||||
menuTitle = GlobalConfig.Menu.Title
|
||||
menuFooter = GlobalConfig.Menu.Footer
|
||||
promptSelect = GlobalConfig.Menu.PromptSelect
|
||||
}
|
||||
|
||||
total := len(ConfiguredSystem)
|
||||
|
||||
s.Mu.Lock()
|
||||
username := s.Username
|
||||
displayTag := s.DisplayTag
|
||||
activeSlots := s.countActiveSlotsLocked()
|
||||
page, totalPages, startIdx, endIdx := menuPageBounds(s.MenuPage, total)
|
||||
|
||||
renderedSlots := make([]slotRenderInfo, endIdx-startIdx)
|
||||
for idx := startIdx; idx < endIdx; idx++ {
|
||||
i := idx - startIdx
|
||||
status := "INACTIVE"
|
||||
var color go3270.Color
|
||||
hasNotice := false
|
||||
if idx < len(s.Slots) && s.Slots[idx] != nil {
|
||||
slot := s.Slots[idx]
|
||||
slot.Mu.Lock()
|
||||
if slot.IsHeld {
|
||||
status = "HELD"
|
||||
color = go3270.Red
|
||||
} else if slot.ScreenChanged {
|
||||
status = "NOTICE"
|
||||
hasNotice = true
|
||||
} else {
|
||||
status = "ACTIVE"
|
||||
color = go3270.Yellow
|
||||
}
|
||||
slot.Mu.Unlock()
|
||||
}
|
||||
renderedSlots[i] = slotRenderInfo{status: status, color: color, hasNotice: hasNotice}
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
|
||||
screen := go3270.Screen{
|
||||
{Row: 0, Col: 0, Content: screenSeparator},
|
||||
{Row: 1, Col: centerCol(menuTitle), Content: menuTitle, Intense: true},
|
||||
{Row: 2, Col: 2, Content: fmt.Sprintf("User: %-10s Session Tag: #%-8s", username, displayTag)},
|
||||
{Row: 2, Col: 58, Content: fmt.Sprintf("Active Sessions: %d", activeSlots)},
|
||||
{Row: 3, Col: 0, Content: screenSeparator},
|
||||
{Row: 5, Col: 2, Content: "CMD ID System Name Description Status", Intense: true},
|
||||
{Row: 6, Col: 2, Content: "--- ---- --------------- ------------------------------- -----------"},
|
||||
}
|
||||
|
||||
row := 7
|
||||
for idx := startIdx; idx < endIdx; idx++ {
|
||||
sys := ConfiguredSystem[idx]
|
||||
info := renderedSlots[idx-startIdx]
|
||||
|
||||
line := fmt.Sprintf("[%2d] %-15.15s %-31.31s %s", idx+1, sys.Name, sys.Description, info.status)
|
||||
screen = append(screen,
|
||||
go3270.Field{
|
||||
Row: row,
|
||||
Col: 2,
|
||||
Name: fmt.Sprintf("cmd_%d", idx+1),
|
||||
Write: true,
|
||||
},
|
||||
go3270.Field{
|
||||
Row: row,
|
||||
Col: 6,
|
||||
Content: line,
|
||||
Intense: info.hasNotice,
|
||||
Color: info.color,
|
||||
},
|
||||
)
|
||||
row++
|
||||
}
|
||||
|
||||
screen = append(screen,
|
||||
go3270.Field{Row: 21, Col: 2, Content: promptSelect},
|
||||
go3270.Field{Row: 21, Col: 18, Name: "opt", Write: true},
|
||||
go3270.Field{Row: 21, Col: 78, Content: ""},
|
||||
go3270.Field{Row: 23, Col: 2, Content: menuFooter},
|
||||
)
|
||||
|
||||
if totalPages > 1 {
|
||||
pageIndicator := fmt.Sprintf("Page %d of %d", page+1, totalPages)
|
||||
pageCol := max(0, 79-len(pageIndicator))
|
||||
screen = append(screen, go3270.Field{Row: 22, Col: pageCol, Content: pageIndicator})
|
||||
}
|
||||
|
||||
return screen
|
||||
}
|
||||
|
||||
func renderMainMenu(s *UserMasterSession, clientConn net.Conn) (go3270.Response, error) {
|
||||
screen := buildMainMenuScreen(s)
|
||||
|
||||
values := make(map[string]string)
|
||||
cp := s.Codepage()
|
||||
|
||||
return go3270.ShowScreenOpts(screen, values, clientConn, go3270.ScreenOpts{
|
||||
Codepage: cp,
|
||||
CursorRow: 21,
|
||||
CursorCol: 19,
|
||||
})
|
||||
}
|
||||
|
||||
func runMenuLoop(s *UserMasterSession, clientConn net.Conn) {
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn == clientConn {
|
||||
s.IsDisconnected = false
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
|
||||
for {
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn != clientConn {
|
||||
s.Mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
|
||||
resp, err := renderMainMenu(s, clientConn)
|
||||
if err != nil {
|
||||
s.Mu.Lock()
|
||||
username, displayTag := s.Username, s.DisplayTag
|
||||
if s.ClientConn == clientConn {
|
||||
s.IsDisconnected = true
|
||||
s.Mu.Unlock()
|
||||
log.Printf("[%s] '%s' Client disconnected while on system menu: %v", displayTag, username, err)
|
||||
cleanupSessionIfEmpty(s)
|
||||
} else {
|
||||
s.Mu.Unlock()
|
||||
log.Printf("[%s] '%s' Previous connection transferred to new terminal", displayTag, username)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch resp.AID {
|
||||
case go3270.AIDPF3:
|
||||
disconnectClient(s, clientConn,
|
||||
"Disconnecting terminal... Session active in background.",
|
||||
"User pressed PF3 to disconnect. Preserving session.")
|
||||
return
|
||||
|
||||
case go3270.AIDPF7, go3270.AIDPF19:
|
||||
s.Mu.Lock()
|
||||
page, _, _, _ := menuPageBounds(s.MenuPage, len(ConfiguredSystem))
|
||||
if page > 0 {
|
||||
s.MenuPage = page - 1
|
||||
} else {
|
||||
s.MenuPage = 0
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
continue
|
||||
|
||||
case go3270.AIDPF8, go3270.AIDPF20:
|
||||
s.Mu.Lock()
|
||||
page, totalPages, _, _ := menuPageBounds(s.MenuPage, len(ConfiguredSystem))
|
||||
if page < totalPages-1 {
|
||||
s.MenuPage = page + 1
|
||||
} else {
|
||||
s.MenuPage = page
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
continue
|
||||
|
||||
case go3270.AIDEnter:
|
||||
s.Mu.Lock()
|
||||
page, _, startIdx, endIdx := menuPageBounds(s.MenuPage, len(ConfiguredSystem))
|
||||
s.MenuPage = page
|
||||
username, displayTag := s.Username, s.DisplayTag
|
||||
isAdmin := s.IsAdmin
|
||||
s.Mu.Unlock()
|
||||
|
||||
connectSlot := -1
|
||||
|
||||
for idx := startIdx; idx < endIdx; idx++ {
|
||||
fieldName := fmt.Sprintf("cmd_%d", idx+1)
|
||||
cmdVal := strings.TrimSpace(resp.Values[fieldName])
|
||||
if cmdVal == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.ToLower(cmdVal) {
|
||||
case "k":
|
||||
killSlot(s, idx)
|
||||
case "h", "r":
|
||||
hold := strings.EqualFold(cmdVal, "h")
|
||||
s.Mu.Lock()
|
||||
if idx < len(s.Slots) && s.Slots[idx] != nil {
|
||||
slot := s.Slots[idx]
|
||||
slot.Mu.Lock()
|
||||
slot.IsHeld = hold
|
||||
if hold {
|
||||
slot.ScreenChanged = false
|
||||
}
|
||||
slot.Mu.Unlock()
|
||||
if hold {
|
||||
log.Printf("[%s] '%s' Placed slot [%d] %s on HOLD", displayTag, username, idx+1, slot.System.Name)
|
||||
} else {
|
||||
log.Printf("[%s] '%s' RELEASED slot [%d] %s from hold", displayTag, username, idx+1, slot.System.Name)
|
||||
}
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
case "b":
|
||||
log.Printf("[%s] '%s' CMD 'B' entered -> starting [%d] %s in background", displayTag, username, idx+1, ConfiguredSystem[idx].Name)
|
||||
ensureSlotConnected(s, idx, true, clientConn)
|
||||
default:
|
||||
if connectSlot == -1 {
|
||||
connectSlot = idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if connectSlot >= 0 {
|
||||
log.Printf("[%s] '%s' CMD entered -> connecting to [%d] %s", displayTag, username, connectSlot+1, ConfiguredSystem[connectSlot].Name)
|
||||
} else {
|
||||
optStr := strings.TrimSpace(resp.Values["opt"])
|
||||
if optStr == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(optStr)
|
||||
cmd := strings.ToLower(parts[0])
|
||||
|
||||
if isAdmin {
|
||||
switch cmd {
|
||||
case "bye":
|
||||
handleAdminBye(s)
|
||||
return
|
||||
case "killall":
|
||||
handleAdminKillAll(s)
|
||||
return
|
||||
case "kill":
|
||||
if len(parts) < 2 {
|
||||
send3270Message(clientConn, "Usage: kill <sessionid>", s.Codepage())
|
||||
time.Sleep(adminNoticeSleep)
|
||||
continue
|
||||
}
|
||||
if handleAdminKill(s, parts[1]) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
selNum, err := strconv.Atoi(optStr)
|
||||
if err != nil || selNum < 1 || selNum > len(ConfiguredSystem) {
|
||||
log.Printf("[%s] '%s' Invalid option '%s' entered by user", displayTag, username, optStr)
|
||||
send3270Message(clientConn, fmt.Sprintf("Invalid option '%s'. Please enter a number between 1 and %d.", optStr, len(ConfiguredSystem)), s.Codepage())
|
||||
time.Sleep(adminNoticeSleep)
|
||||
continue
|
||||
}
|
||||
connectSlot = selNum - 1
|
||||
log.Printf("[%s] '%s' User selected option %d -> connecting to [%d] %s", displayTag, username, selNum, selNum, ConfiguredSystem[connectSlot].Name)
|
||||
}
|
||||
|
||||
connectOrResumeSlot(s, connectSlot, clientConn)
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn != clientConn || s.IsDisconnected {
|
||||
s.Mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.Mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func disconnectClient(s *UserMasterSession, clientConn net.Conn, msg, logMsg string) {
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn == clientConn {
|
||||
s.IsDisconnected = true
|
||||
s.ActiveSlotIdx = -1
|
||||
username, displayTag := s.Username, s.DisplayTag
|
||||
cp := s.codepageLocked()
|
||||
s.Mu.Unlock()
|
||||
log.Printf("[%s] '%s' %s", displayTag, username, logMsg)
|
||||
send3270Message(clientConn, msg, cp)
|
||||
_ = clientConn.Close()
|
||||
cleanupSessionIfEmpty(s)
|
||||
} else {
|
||||
s.Mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleResumeSession(s *UserMasterSession, clientConn net.Conn) {
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn == clientConn {
|
||||
s.IsDisconnected = false
|
||||
}
|
||||
username, displayTag := s.Username, s.DisplayTag
|
||||
activeSlots := s.countActiveSlotsLocked()
|
||||
cp := s.codepageLocked()
|
||||
s.Mu.Unlock()
|
||||
|
||||
log.Printf("[%s] '%s' has active background sessions. Presenting resume", displayTag, username)
|
||||
|
||||
menuTitle := ""
|
||||
if GlobalConfig != nil {
|
||||
menuTitle = GlobalConfig.Menu.Title
|
||||
}
|
||||
|
||||
screen := go3270.Screen{
|
||||
{Row: 0, Col: 0, Content: screenSeparator},
|
||||
{Row: 1, Col: centerCol(menuTitle), Content: menuTitle, Intense: true},
|
||||
{Row: 2, Col: 2, Content: fmt.Sprintf("User: %-10s Session Tag: #%-8s", username, displayTag)},
|
||||
{Row: 2, Col: 58, Content: fmt.Sprintf("Active Sessions: %2d", activeSlots)},
|
||||
{Row: 3, Col: 0, Content: screenSeparator},
|
||||
|
||||
{Row: 10, Col: 14, Content: fmt.Sprintf("Welcome back, %s!", username)},
|
||||
|
||||
{Row: 22, Col: 2, Content: "[Enter] System Menu [PF3] Disconnect"},
|
||||
}
|
||||
|
||||
values := make(map[string]string)
|
||||
resp, err := go3270.ShowScreenOpts(screen, values, clientConn, go3270.ScreenOpts{
|
||||
Codepage: cp,
|
||||
CursorRow: 22,
|
||||
CursorCol: 2,
|
||||
})
|
||||
if err != nil {
|
||||
s.Mu.Lock()
|
||||
if s.ClientConn == clientConn {
|
||||
s.IsDisconnected = true
|
||||
s.ActiveSlotIdx = -1
|
||||
s.Mu.Unlock()
|
||||
log.Printf("[%s] '%s' disconnected during resume prompt (%v)", displayTag, username, err)
|
||||
cleanupSessionIfEmpty(s)
|
||||
} else {
|
||||
s.Mu.Unlock()
|
||||
log.Printf("[%s] '%s' Connection transferred during resume prompt", displayTag, username)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if resp.AID == go3270.AIDPF3 {
|
||||
disconnectClient(s, clientConn, "Disconnecting... ", "PF3 disconnect during resume prompt")
|
||||
return
|
||||
}
|
||||
|
||||
runMenuLoop(s, clientConn)
|
||||
}
|
||||
|
||||
func menuPageBounds(menuPage, totalSystems int) (page, totalPages, startIdx, endIdx int) {
|
||||
totalPages = (totalSystems + 12) / 13
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
if menuPage < 0 {
|
||||
page = 0
|
||||
} else if menuPage >= totalPages {
|
||||
page = totalPages - 1
|
||||
} else {
|
||||
page = menuPage
|
||||
}
|
||||
startIdx = page * 13
|
||||
endIdx = startIdx + 13
|
||||
if endIdx > totalSystems {
|
||||
endIdx = totalSystems
|
||||
}
|
||||
return page, totalPages, startIdx, endIdx
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/racingmars/go3270"
|
||||
)
|
||||
|
||||
// Telnet Command Constants (RFC 854 / RFC 885)
|
||||
const (
|
||||
IAC byte = 0xFF
|
||||
DONT byte = 0xFE
|
||||
DO byte = 0xFD
|
||||
WONT byte = 0xFC
|
||||
WILL byte = 0xFB
|
||||
SB byte = 0xFA
|
||||
GA byte = 0xF9
|
||||
NOP byte = 0xF1
|
||||
SE byte = 0xF0
|
||||
EOR byte = 0xEF
|
||||
|
||||
OPT_BINARY byte = 0x00
|
||||
OPT_SUPPRESS_GA byte = 0x03
|
||||
OPT_TIME_MARK byte = 0x06
|
||||
OPT_TERMINAL_TYPE byte = 0x18
|
||||
OPT_EOR byte = 0x19
|
||||
OPT_TN3270E byte = 0x28
|
||||
|
||||
// Terminal-Type Subnegotiation Codes (RFC 1091)
|
||||
TTYPE_IS byte = 0x00
|
||||
TTYPE_SEND byte = 0x01
|
||||
|
||||
// TN3270E Subnegotiation Command Codes (RFC 2355)
|
||||
TN3270E_DEVICE_TYPE byte = 0x02
|
||||
TN3270E_FUNCTIONS byte = 0x03
|
||||
TN3270E_IS byte = 0x04
|
||||
TN3270E_REJECT byte = 0x06
|
||||
TN3270E_REQUEST byte = 0x07
|
||||
TN3270E_SEND byte = 0x08
|
||||
|
||||
// Data Types in TN3270E Header
|
||||
TN3270E_DATA_3270 byte = 0x00
|
||||
TN3270E_DATA_RESP byte = 0x02
|
||||
TN3270E_DATA_BIND byte = 0x03
|
||||
TN3270E_DATA_UNBIND byte = 0x04
|
||||
TN3270E_DATA_SSCP_LU byte = 0x07
|
||||
|
||||
// Functions & Header Flags in RFC 2355
|
||||
TN3270E_FUNC_BIND_IMAGE byte = 0x00
|
||||
TN3270E_FUNC_RESPONSES byte = 0x02
|
||||
TN3270E_RESP_ALWAYS byte = 0x02
|
||||
TN3270E_UNBIND_NORMAL byte = 0x01
|
||||
TN3270E_MAX_SEQ uint16 = 0x7FFF
|
||||
|
||||
// 3270 Command Codes
|
||||
CMD_WRITE byte = 0xF1
|
||||
CMD_WSF byte = 0xF3
|
||||
CMD_ERASE_WRITE byte = 0xF5
|
||||
CMD_ERASE_WRITE_SNA byte = 0x05
|
||||
CMD_ERASE_WRITE_ALT byte = 0x7E
|
||||
CMD_ERASE_WRITE_ALT_SNA byte = 0x0D
|
||||
CMD_ERASE_ALL_UNPROTECTED byte = 0x6F
|
||||
|
||||
// 3270 Structured Field & Query Reply Constants
|
||||
AID_QUERY_REPLY byte = 0x88
|
||||
SF_READ_PARTITION byte = 0x01
|
||||
SF_RP_QUERY byte = 0xFF
|
||||
SF_QUERY_REPLY byte = 0x81
|
||||
QR_USABLE_AREA byte = 0x81
|
||||
QR_COLOR byte = 0x86
|
||||
QR_HIGHLIGHTING byte = 0x87
|
||||
QR_IMPLICIT_PARTITION byte = 0xA6
|
||||
QR_NULL byte = 0xFF
|
||||
|
||||
// 3270 Extended Color & Highlighting Attribute Constants
|
||||
ATTR_COLOR_BLUE byte = 0xF1
|
||||
ATTR_COLOR_RED byte = 0xF2
|
||||
ATTR_COLOR_PINK byte = 0xF3
|
||||
ATTR_COLOR_GREEN byte = 0xF4
|
||||
ATTR_COLOR_TURQUOISE byte = 0xF5
|
||||
ATTR_COLOR_YELLOW byte = 0xF6
|
||||
ATTR_COLOR_WHITE byte = 0xF7
|
||||
|
||||
ATTR_HL_BLINK byte = 0xF1
|
||||
ATTR_HL_REVERSE byte = 0xF2
|
||||
ATTR_HL_UNDERSCORE byte = 0xF4
|
||||
)
|
||||
|
||||
type LoginCredentials struct {
|
||||
Username string
|
||||
Password string
|
||||
SessionKey string
|
||||
}
|
||||
|
||||
type AuthResult struct {
|
||||
OK bool
|
||||
Reason string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
type TargetSystem struct {
|
||||
Name string `toml:"name"`
|
||||
Description string `toml:"description"`
|
||||
HostPort string `toml:"host_port"`
|
||||
TLS bool `toml:"tls"`
|
||||
InsecureSkipVerify bool `toml:"insecure_skip_verify"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
BindAddress string `toml:"bind_address"`
|
||||
Codepage string `toml:"codepage"`
|
||||
TLS bool `toml:"tls"`
|
||||
CertFile string `toml:"cert_file"`
|
||||
KeyFile string `toml:"key_file"`
|
||||
Public bool `toml:"public"`
|
||||
}
|
||||
|
||||
type LoginConfig struct {
|
||||
Title string `toml:"title"`
|
||||
Subtitle string `toml:"subtitle"`
|
||||
PromptUsername string `toml:"prompt_username"`
|
||||
PromptPassword string `toml:"prompt_password"`
|
||||
Instructions string `toml:"instructions"`
|
||||
}
|
||||
|
||||
type MenuConfig struct {
|
||||
Title string `toml:"title"`
|
||||
PromptSelect string `toml:"prompt_select"`
|
||||
Footer string `toml:"footer"`
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
Username string `toml:"username"`
|
||||
Hash string `toml:"hash"`
|
||||
Password string `toml:"password"`
|
||||
Admin bool `toml:"admin"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Public bool `toml:"public"`
|
||||
Server ServerConfig `toml:"server"`
|
||||
Login LoginConfig `toml:"login"`
|
||||
Menu MenuConfig `toml:"menu"`
|
||||
Systems []TargetSystem `toml:"systems"`
|
||||
Users []UserConfig `toml:"users"`
|
||||
}
|
||||
|
||||
type SessionSlot struct {
|
||||
System TargetSystem
|
||||
Upstream net.Conn
|
||||
LastScreen []byte
|
||||
ScreenChanged bool
|
||||
IsTN3270E bool
|
||||
IsBound bool
|
||||
SeqNumber uint16
|
||||
IsHeld bool
|
||||
MaxBufferSize int
|
||||
Mu sync.Mutex
|
||||
}
|
||||
|
||||
type slotRenderInfo struct {
|
||||
status string
|
||||
color go3270.Color
|
||||
hasNotice bool
|
||||
}
|
||||
|
||||
type UserMasterSession struct {
|
||||
Username string
|
||||
SessionKey string
|
||||
DisplayTag string
|
||||
TerminalType string
|
||||
DevInfo go3270.DevInfo
|
||||
ClientConn net.Conn
|
||||
ActiveSlotIdx int
|
||||
Slots []*SessionSlot
|
||||
IsDisconnected bool
|
||||
MenuPage int
|
||||
IsAdmin bool
|
||||
Mu sync.Mutex
|
||||
}
|
||||
|
||||
func (u *UserConfig) GetHash() string {
|
||||
if u.Hash != "" {
|
||||
return u.Hash
|
||||
}
|
||||
return u.Password
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) Identity() (username, displayTag string) {
|
||||
s.Mu.Lock()
|
||||
defer s.Mu.Unlock()
|
||||
return s.Username, s.DisplayTag
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) Codepage() go3270.Codepage {
|
||||
s.Mu.Lock()
|
||||
defer s.Mu.Unlock()
|
||||
return s.codepageLocked()
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) codepageLocked() go3270.Codepage {
|
||||
if s.DevInfo != nil {
|
||||
return s.DevInfo.Codepage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) CountActiveSlots() int {
|
||||
s.Mu.Lock()
|
||||
defer s.Mu.Unlock()
|
||||
return s.countActiveSlotsLocked()
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) countActiveSlotsLocked() int {
|
||||
count := 0
|
||||
for _, slot := range s.Slots {
|
||||
if slot != nil {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (s *UserMasterSession) GetMatrix() (rows, cols, maxBuf int) {
|
||||
s.Mu.Lock()
|
||||
defer s.Mu.Unlock()
|
||||
return GetTerminalMatrix(s.TerminalType, s.DevInfo)
|
||||
}
|
||||
Reference in New Issue
Block a user