initial commit
This commit is contained in:
+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
|
||||
}
|
||||
Reference in New Issue
Block a user