484 lines
12 KiB
Go
484 lines
12 KiB
Go
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()
|
|
if hold {
|
|
slot.IsHeld = true
|
|
slot.ScreenChanged = false
|
|
} else {
|
|
slot.resetHoldAndLoopCountersLocked()
|
|
}
|
|
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
|
|
}
|