Files
ptb/tcpserver.go
Torsten Harenberg dfca7ed80c Fix data loss on TCP reconnect:
1. Fixed ByteFIFO (fifo.go): Changed Dequeue(), DequeueOrWait(), and DequeueOrWaitContext() to always copy() data into a new byte slice instead of just slicing the buffer.
  2. Fixed TCP server logic (tcpserver.go): Changed the send goroutine to poll for data availability (without dequeueing) using GetLen(), then sleep briefly to batch incoming data,
  and finally dequeue all available data at once. This avoids the race condition where we were splitting dequeue operations and losing bytes in between.
2025-10-30 09:34:50 +01:00

387 lines
9.9 KiB
Go

package main
import (
"bufio"
"context"
"errors"
"fmt"
"log"
"net"
"regexp"
"strings"
"time"
"github.com/TwiN/go-color"
)
// Chunks splits a string into chunks of chunkSize length
// by topskip taken from https://stackoverflow.com/questions/18556693/slice-string-into-letters
func Chunks(s string, chunkSize int) []string {
if len(s) == 0 {
return nil
}
if chunkSize >= len(s) {
return []string{s}
}
chunks := make([]string, 0, (len(s)-1)/chunkSize+1)
currentLen := 0
currentStart := 0
for i := range s {
if currentLen == chunkSize {
chunks = append(chunks, s[currentStart:i])
currentLen = 0
currentStart = i
}
currentLen++
}
chunks = append(chunks, s[currentStart:])
return chunks
}
// HandleConnection processes incoming connections
func handleTCPCmdConnection(conn net.Conn) {
defer func() {
s.Status &^= StatusTCPCmdActive
s.Command.Cmd.Enqueue("DD") // force stop all connetions if TCP connection is closing
err := conn.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
}()
s.Status |= StatusTCPCmdActive
// Sending "IAMLIVE" every 60 seconds
alivectx, alivecancel := context.WithCancel(context.Background())
go func(conn net.Conn, ctx context.Context) {
for ctx.Err() == nil {
if s.VARAMode {
s.Command.Response.Enqueue("IAMALIVE")
}
time.Sleep(60 * time.Second)
}
}(conn, alivectx)
defer alivecancel()
/*
bufferctx, buffercancel := context.WithCancel(context.Background())
go func(conn net.Conn, ctx context.Context) {
for ctx.Err() == nil {
if s.VARAMode {
s.Command.Response.Enqueue(fmt.Sprintf("BUFFER %d", s.Data.Data.GetLen()))
}
time.Sleep(10 * time.Second)
}
}(conn, bufferctx)
defer buffercancel()
*/
s.Protocol <- fmt.Sprintf(color.InGreen("TCP Cmd Connection established with %s\n"), conn.RemoteAddr())
s.Status |= StatusTCPCmdActive
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func(ctx context.Context) {
for ctx.Err() == nil {
msg, err := s.Command.Response.DequeueOrWaitContext(ctx)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InRed("End of Cmd Data Connection %s\n"), conn.RemoteAddr())
return
} else {
s.Protocol <- fmt.Sprintf(color.InCyan("Response: %s\n"), msg)
_, err = conn.Write([]byte(fmt.Sprintf("%s\r", msg)))
if err != nil {
writeDebug(err.Error(), 0)
}
}
}
}(ctx)
reader := bufio.NewReader(conn)
for {
// Read incoming message
message, err := reader.ReadString('\r')
if err != nil {
//s.Protocol <- fmt.Sprintf(color.InRed("Connection closed by %s\n"), conn.RemoteAddr())
s.Status &^= StatusTCPCmdActive
alivecancel()
//buffercancel()
cancel()
err = conn.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
break
}
message = strings.TrimSpace(message)
re := regexp.MustCompile("[[:^ascii:]]")
message = re.ReplaceAllLiteralString(message, "") // prevent any non-ASCII from being sent
if message == "" { // do not send an empty message to the controller
continue
}
// Process the VARA TNC protocol commands
if s.VARAMode {
translatedmsg, predefanswer, err := processVARACommand(message)
if err != nil {
s.Protocol <- color.InRed(fmt.Sprintf("Error translating VARA command %s: %s\n", message, err.Error()))
log.Println(fmt.Sprintf("Error translating VARA command %s: %s\n", message, err.Error()))
s.Command.Response.Enqueue("WRONG")
} else {
if translatedmsg != "" {
s.Protocol <- fmt.Sprintf(color.InPurple("(V) Got: %s Sending: %s\n"), message, translatedmsg)
s.Command.Cmd.Enqueue(translatedmsg)
}
if predefanswer != "" {
s.Command.Response.Enqueue(predefanswer)
} else {
s.Command.Response.Enqueue("OK")
}
}
} else { // TCP Mode == passthrough
s.Protocol <- fmt.Sprintf(color.InPurple("(T) Sending: %s\n"), message)
s.Command.Cmd.Enqueue(message)
}
}
}
func handleTCPDataConnection(conn net.Conn) {
defer func() {
s.Status &^= StatusTCPDataActive
err := conn.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
}()
s.Protocol <- fmt.Sprintf(color.InGreen("TCP Data Connection established with %s\n"), conn.RemoteAddr())
s.Status |= StatusTCPDataActive
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Clear only the outgoing data buffer (Data.Data) to discard any unsent data from previous connection
// But keep Data.Response so we don't lose any data received from the modem that should be sent to the client
s.Data.Data.Clear()
go func() {
for ctx.Err() == nil {
writeDebug("TCP Data send goroutine: waiting for data...", 1)
// Wait until buffer has at least 1 byte without dequeueing
for {
if ctx.Err() != nil {
writeDebug("TCP Data send goroutine: context cancelled, exiting", 1)
return
}
bufLen := s.Data.Response.GetLen()
if bufLen > 0 {
writeDebug(fmt.Sprintf("TCP Data send goroutine: buffer has %d bytes", bufLen), 1)
// Sleep briefly to allow more data to arrive
time.Sleep(10 * time.Millisecond)
// Dequeue ALL available data
bufLen = s.Data.Response.GetLen()
msg, err := s.Data.Response.Dequeue(bufLen)
if err == nil {
writeDebug(fmt.Sprintf("TCP Data send goroutine: sending %d bytes to client", len(msg)), 1)
//TODO: VARA
if !s.DaemonMode {
err := s.FromPactor.Enqueue(msg)
if err != nil {
writeDebug(err.Error(), 0)
}
}
_, err = conn.Write(msg)
if err != nil {
writeDebug(err.Error(), 0)
}
}
break
}
// Sleep a bit before checking again
time.Sleep(1 * time.Millisecond)
}
}
}()
reader := bufio.NewReader(conn)
for {
buf := make([]byte, 1024)
n, err := reader.Read(buf)
temp := buf[:n]
if err != nil {
s.Status &^= StatusTCPDataActive
cancel()
err := conn.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
return
}
//TODO: muss das nicht weg?
/* if temp == 0x04 {
break
}
*/
if !s.DaemonMode {
err := s.ToPactor.Enqueue(temp)
if err != nil {
writeDebug(err.Error(), 0)
}
}
if n > 255 {
// we can dump at max 255 bytes into
for _, ck := range Chunks(string(temp), 255) {
err := s.Data.Data.Enqueue([]byte(ck))
if err != nil {
writeDebug(err.Error(), 0)
}
}
} else {
err := s.Data.Data.Enqueue(temp)
if err != nil {
writeDebug(err.Error(), 0)
}
}
}
}
// Process VARA commands and return appropriate WA8DED command and a pre-defined answer
func processVARACommand(command string) (string, string, error) {
switch {
case strings.HasPrefix(command, "CONNECT"):
re, err := regexp.MatchString(`CONNECT \w+ \w+`, command)
if err != nil || !re {
return "", "", errors.New("error matching regex")
}
c := strings.Split(command, " ")
return fmt.Sprintf("C %s", c[2]), "", nil
case strings.HasPrefix(command, "DISCONNECT"):
return "D", "", nil
case strings.HasPrefix(command, "ABORT"):
return "DD", "", nil
// case strings.HasPrefix(command, "VERSION"):
// return "", nil
case strings.HasPrefix(command, "LISTEN"):
// Handle listen
if strings.HasSuffix(command, "ON") {
return "%L 1", "", nil
}
if strings.HasSuffix(command, "OFF") {
return "%L 0", "", nil
}
return "", "", errors.New("neither ON nor OFF after LISTEN")
case strings.HasPrefix(command, "MYCALL"):
m := ""
// Handle MYCALL
s := strings.Split(command, " ")
if len(s) > 1 {
// send own callsign to PACTOR controller
m = s[1]
} else {
return "", "", errors.New("invalid MYCALL command")
}
return fmt.Sprintf("%s%s", "I ", m), "", nil
case strings.HasPrefix(command, "COMPRESSION"):
// Handle COMPRESSION
return "", "", nil
case strings.HasPrefix(command, "BW"):
// Has no meaning for PACTOR
return "", "", nil
case strings.HasPrefix(command, "CHAT"):
// Has no meaning for PACTOR
return "", "", nil
case strings.HasPrefix(command, "WINLINK SESSION"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "P2P SESSION"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "CWID"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "PUBLIC"):
// Has no meaning for PACTOR, just return "OK"
return "", "", nil
case strings.HasPrefix(command, "VERSION"):
return "", "VERSION 1.0.0", nil
default:
// Handle unrecognized commands
return "", "", errors.New("unknown command")
}
}
func tcpCmdServer(Config *Userconfig) {
// Start listening for connections
listener, err := net.Listen("tcp", Config.ServerAddress)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InWhiteOverRed("Error starting server: %v\n"), err)
log.Println(err)
return
}
defer func() {
err := listener.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
}()
s.Protocol <- fmt.Sprintf("TCP Protocol Server listening on %s\n", Config.ServerAddress)
for {
// Accept incoming connection
conn, err := listener.Accept()
if err != nil {
s.Protocol <- fmt.Sprintf("Error accepting connection: %v\n", err)
continue
}
// don't handle in goroutine as you normally would. There shouldn't be more than one connection.
handleTCPCmdConnection(conn)
}
}
func tcpDataServer(Config *Userconfig) {
listener, err := net.Listen("tcp", Config.DataAddress)
if err != nil {
s.Protocol <- fmt.Sprintf(color.InWhiteOverRed("Error starting server: %v\n"), err)
log.Println(err)
return
}
defer func() {
err := listener.Close()
if err != nil {
writeDebug(err.Error(), 0)
}
}()
s.Protocol <- fmt.Sprintf("TCP Data Server listening on %s\n", Config.DataAddress)
for {
// Accept incoming connection
conn, err := listener.Accept()
if err != nil {
s.Protocol <- fmt.Sprintf("Error accepting connection: %v\n", err)
continue
}
// don't handle in goroutine as you normally would. There shouldn't be more than one connection.
handleTCPDataConnection(conn)
}
}