commit d911248c53247066b38bdfa98d5b240b0e5ee7c5 Author: Bruno Rybársky Date: Sat Jun 8 11:52:12 2024 +0200 Init diff --git a/client.go b/client.go new file mode 100644 index 0000000..6be4994 --- /dev/null +++ b/client.go @@ -0,0 +1,55 @@ +package main + +import ( + "github.com/BRNSystems/go-telnet" + "strconv" +) + +type Client struct { + Client telnet.Client + Username string + ID int + History []string + HistoryIndex int + context telnet.Context + writer telnet.Writer + reader telnet.Reader + // Add more fields as needed +} + +func NewClient(context telnet.Context, writer telnet.Writer, reader telnet.Reader) int { + client := Client{} + + clientsMutex.Lock() + clients = append(clients, &client) + clientID := len(clients) - 1 + defer clientsMutex.Unlock() + + clients[clientID].ID = clientID + clients[clientID].Username = "Client #" + strconv.Itoa(clientID) + clients[clientID].context = context + clients[clientID].writer = writer + clients[clientID].reader = reader + return clientID +} + +func RemoveClient(clientID int) { + clientsMutex.Lock() + defer clientsMutex.Unlock() + clients = append(clients[:clientID], clients[clientID+1:]...) +} + +func (client Client) send(message string) { + client.writer.Write([]byte(message)) +} + +func getIDByName(name string) int { + clientsMutex.Lock() + defer clientsMutex.Unlock() + for _, client := range clients { + if client.Username == name { + return client.ID + } + } + return -1 +} diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..115ec8b --- /dev/null +++ b/commands.go @@ -0,0 +1,121 @@ +package main + +import ( + "math/rand" + "strconv" + "sync" +) + +type CommandHandlerFunc func(args []string, clientID int) string + +type CommandRegistry struct { + commands map[string]CommandHandlerFunc + mutex sync.Mutex // Mutex to safely access commands map +} + +func NewCommandRegistry() *CommandRegistry { + return &CommandRegistry{ + commands: make(map[string]CommandHandlerFunc), + } +} + +func (cr *CommandRegistry) RegisterCommand(command string, handler CommandHandlerFunc) { + cr.mutex.Lock() + defer cr.mutex.Unlock() + cr.commands[command] = handler +} + +func (cr *CommandRegistry) GetCommandHandler(command string, clientID int) (CommandHandlerFunc, bool, int) { + cr.mutex.Lock() + defer cr.mutex.Unlock() + handler, ok := cr.commands[command] + return handler, ok, clientID +} + +func RegisterCommands(registry *CommandRegistry) { + registry.RegisterCommand("help", HelpCommand) + registry.RegisterCommand("setname", SetNameCommand) + registry.RegisterCommand("list", ListClientsCommand) + registry.RegisterCommand("tell", TellClientCommand) + registry.RegisterCommand("clear", ClearCommand) + registry.RegisterCommand("rng", RandomCommand) + // Add more commands here... +} + +func HelpCommand(args []string, clientID int) string { + // Handle help command + if len(clients) > 0 { + return "Just help yourself, " + clients[clientID].Username + } + return "No clients available" +} + +func RandomCommand(args []string, clientID int) string { + // Handle help command + if len(args) == 2 { + from, err1 := strconv.Atoi(args[0]) + to, err2 := strconv.Atoi(args[1]) + if err1 != nil || err2 != nil { + return "Invalid argument" + } + if to-from <= 0 { + return "Invalid range" + } + return "The number is " + strconv.Itoa(rand.Intn(to-from)+from) + } + return "No range provided" +} + +func ClearCommand(args []string, clientID int) string { + // Handle help command + return "\033[H\033[2J\033[K" +} + +func SetNameCommand(args []string, clientID int) string { + // Handle help command + if len(args) == 1 { + newName := args[0] + clients[clientID].Username = newName + return "Name set to " + newName + } + return "No name provided" +} + +func ListClientsCommand(args []string, clientID int) string { + message := "" + for i, client := range clients { + message += strconv.Itoa(i) + ": " + client.Username + "\n" + } + return message +} + +func TellClientCommand(args []string, clientID int) string { + message := "Message not delivered" + if len(args) >= 2 { + clientTo := args[0] + //if integer assume clientID + var toClientID int + var toClientName string + toClientID, err := strconv.Atoi(clientTo) + if err != nil { + toClientID = getIDByName(clientTo) + toClientName = clientTo + } + if toClientID < len(clients) && toClientID >= 0 { + toClientName = clients[toClientID].Username + myUsername := clients[clientID].Username + newMessage := "" + //all the remaining arguments are a part of the message, join them with space + for i, arg := range args { + if i > 0 { + newMessage += arg + " " + } + } + clients[toClientID].send("\a\r\n<" + myUsername + "(" + strconv.Itoa(clientID) + ")> " + newMessage + "\r\n") + return "Message sent to " + toClientName + } else { + message = "Invalid recipient" + } + } + return message +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3fdac5f --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module telnetroulette + +go 1.22 + +require ( + github.com/BRNSystems/go-telnet v0.0.0-20240607215108-c7f613a868ed + github.com/reiver/go-oi v1.0.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c887ee3 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/BRNSystems/go-telnet v0.0.0-20240607213850-6f6b77773107 h1:mch1qDcG/Cj3c+RKlQiTSgtd1ijhFHG986HtEuvJCgA= +github.com/BRNSystems/go-telnet v0.0.0-20240607213850-6f6b77773107/go.mod h1:xZD4mQdIL/DseYfMKq66/DDBwxmX48EmqjlMY0opBDg= +github.com/BRNSystems/go-telnet v0.0.0-20240607215108-c7f613a868ed h1:ogTX00ebS+daoXPZJ869USmnn1WIrYkZg+v944meYio= +github.com/BRNSystems/go-telnet v0.0.0-20240607215108-c7f613a868ed/go.mod h1:xZD4mQdIL/DseYfMKq66/DDBwxmX48EmqjlMY0opBDg= +github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM= +github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI= diff --git a/main.go b/main.go new file mode 100644 index 0000000..143e20b --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/BRNSystems/go-telnet" + "sync" +) + +var clients []*Client +var clientsMutex sync.Mutex + +func main() { + addr := ":6969" + commandRegistry := NewCommandRegistry() + termHandler := NewInternalTerminalHandler("# ", + ` ____ _ _ ____ _ _ _ +/ ___|| |__ ___ | |_ __ _ _ _ _ __ | _ \ ___ _ _| | ___| |_| |_ ___ +\___ \| '_ \ / _ \| __/ _`+"`"+` | | | | '_ \ | |_) / _ \| | | | |/ _ \ __| __/ _ \ + ___) | | | | (_) | || (_| | |_| | | | | | _ < (_) | |_| | | __/ |_| || __/ +|____/|_| |_|\___/ \__\__, |\__,_|_| |_| |_| \_\___/ \__,_|_|\___|\__|\__\___| + |___/`, + commandRegistry) + + // Register commands + RegisterCommands(commandRegistry) + + if err := telnet.ListenAndServe(addr, termHandler); nil != err { + panic(err) + } +} diff --git a/terminal_handler.go b/terminal_handler.go new file mode 100644 index 0000000..4726312 --- /dev/null +++ b/terminal_handler.go @@ -0,0 +1,108 @@ +package main + +import ( + "github.com/BRNSystems/go-telnet" + "github.com/reiver/go-oi" + "strings" +) + +type InternalTerminalHandler struct { + ShellCharacter string + WelcomeMessage string + CommandRegistry *CommandRegistry +} + +func NewInternalTerminalHandler(shellCharacter, welcomeMessage string, commandRegistry *CommandRegistry) *InternalTerminalHandler { + return &InternalTerminalHandler{ + ShellCharacter: shellCharacter, + WelcomeMessage: welcomeMessage, + CommandRegistry: commandRegistry, + } +} + +func (handler *InternalTerminalHandler) ServeTELNET(ctx telnet.Context, w telnet.Writer, r telnet.Reader) { + clientID := NewClient(ctx, w, r) + + defer RemoveClient(clientID) + w.RawWrite([]byte{0xff, 0xfb, 0x03}) //send char by char + w.RawWrite([]byte{0xff, 0xfb, 0x01}) //echo from server + w.Write([]byte(strings.ReplaceAll(handler.WelcomeMessage, "\n", "\r\n") + "\r\n")) + //replace every \n with \r\n in WelcomeMessage + w.Write([]byte(handler.ShellCharacter)) + + var lineBuffer []byte + + CSISequence := 0 + + for { + var bt [1]byte + n, err := r.Read(bt[:]) + if err != nil { + break + } + + if n > 0 { + b := bt[0] + + if b == 0x03 || b == 0x04 { + return + } else if b == 0x1B { //escape + CSISequence = 1 + } else if CSISequence == 1 && b == 0x5B { + CSISequence = 2 + } else if CSISequence == 2 && b == 'A' { + if len(lineBuffer) == 0 && len(clients[clientID].History) > 0 { + if clients[clientID].HistoryIndex < len(clients[clientID].History)-1 { + clients[clientID].HistoryIndex++ + } + lineBuffer = []byte(clients[clientID].History[clients[clientID].HistoryIndex]) + w.RawWrite([]byte{0x1B, 0x5B, 0x4B}) //clear line + w.Write(lineBuffer) + } + } else if CSISequence == 2 && b == 'B' { + if len(lineBuffer) == 0 && len(clients[clientID].History) > 0 { + if clients[clientID].HistoryIndex > 0 { + clients[clientID].HistoryIndex-- + } + lineBuffer = []byte(clients[clientID].History[clients[clientID].HistoryIndex]) + w.RawWrite([]byte{0x1B, 0x5B, 0x4B}) //clear line + w.Write(lineBuffer) + } + } else if b == 0x7F { + if len(lineBuffer) > 0 { + w.Write([]byte{0x08, 0x20, 0x08}) + lineBuffer = lineBuffer[:len(lineBuffer)-1] + } + } else if b != '\r' && b != '\n' && b != 0 { + CSISequence = 0 + lineBuffer = append(lineBuffer, b) + w.RawWrite([]byte{b}) + } else if b == '\r' { + w.Write([]byte("\r\n")) + command := string(lineBuffer) + args := strings.Fields(command) + var message string + + if len(args) > 0 { + cmd := args[0] + if cmd == "exit" { + return + } + handlerFunc, ok, clientID := handler.CommandRegistry.GetCommandHandler(cmd, clientID) + clients[clientID].HistoryIndex = 0 + if ok { + message = handlerFunc(args[1:], clientID) + clients[clientID].History = append(clients[clientID].History, command) + } else { + message = "Command not found" + } + message += "\n" + handler.ShellCharacter + } else { + message = handler.ShellCharacter + } + oi.LongWrite(w, []byte(strings.ReplaceAll(message, "\n", "\r\n"))) + lineBuffer = nil + } + } + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..50eb8fd --- /dev/null +++ b/util.go @@ -0,0 +1,14 @@ +package main + +import ( + "unicode" +) + +func isInt(s string) bool { + for _, c := range s { + if !unicode.IsDigit(c) { + return false + } + } + return true +}