commit d47db7ec70fe8cd2c2c352f2576c0fc06ddd9e26 Author: Robert Date: Sat Feb 8 20:16:39 2025 -0500 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85a2eab --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +chat_client.yaml +chat_server.yaml +chat_client +chat_server +make_a_key +go.sum diff --git a/BasicChat.desktop b/BasicChat.desktop new file mode 100755 index 0000000..fb03643 --- /dev/null +++ b/BasicChat.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Terminal=false +Exec=/opt/basic_chat/chat_client +Path=/opt/basic_chat +Name=basicChat +Comment=basic_chat +Icon=/opt/basic_chat/basic_chat.png diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..07bca8c --- /dev/null +++ b/INSTALL @@ -0,0 +1,50 @@ +## DO NOT do as ROOT USER!!! sudo where needed instead +$ git clone basic_chat +$ sudo mkdir -p /opt/basic_chat +$ sudo chown $USER:$USER /opt/basic_chat +$ sudo mv $(pwd)/basic_chat /opt/basic_chat/ +$ pushd /opt/basic_chat +# --------------------------------------------------------------- +Make a secure KEY: +$ go mod tidy +$ go build make_a_key.go +$ ./make_a_key +# --------------------------------------------------------------- + +Server Install: +$ nano chat_server.yaml +# Replace key with new Key +$ go build chat_server.go + +$ sudo ln -s $(pwd)/basic_chat_server.service /etc/systemd/system/ + +Reload the service files to include the new service. +$ sudo systemctl daemon-reload + +Start your service -- HINT type basic_ ((TAB Key)) +$ sudo systemctl start basic_chat_server.service + +To check the status of your service +$ sudo systemctl status basic_chat_server.service + +To enable your service on every reboot +$ sudo systemctl enable basic_chat_server.service + +To disable your service on every reboot +sudo systemctl disable basic_chat_server.service + +UPDATE firewall rules for port 8080 or whatever else is needed... +$ sudo ufw allow 8080/tcp +$ sudo ufw status +# If all looks well, then $ sudo ufw enable + +# --------------------------------------------------------------- +Client Install: +$ nano chat_client.yaml +# Replace key with new Key, also Update IP to Server +$ go build chat_client.go +$ sudo ln -s $(pwd)/BasicChat.desktop /usr/share/applications/ +$ sudo update-desktop-database /usr/share/applications/ +# ---------------------------------------------------------------- +Done: +$ popd diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..094a1be --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2025 Robert Strutts + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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 OR COPYRIGHT HOLDERS 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. diff --git a/README b/README new file mode 100644 index 0000000..c82a274 --- /dev/null +++ b/README @@ -0,0 +1,7 @@ +# Basic Chat +Author: By Robert Strutts +## License MIT - Copyright 2025 - Robert Strutts +# View INSTALL guide for help +``` +cat INSTALL +``` diff --git a/basic_chat.png b/basic_chat.png new file mode 100644 index 0000000..63b9ad2 Binary files /dev/null and b/basic_chat.png differ diff --git a/basic_chat_server.service b/basic_chat_server.service new file mode 100644 index 0000000..f60aa9a --- /dev/null +++ b/basic_chat_server.service @@ -0,0 +1,11 @@ +[Unit] +Description="Basic Chat Server" + +[Service] +User=nobody +WorkingDirectory=/opt/basic_chat +ExecStart=/opt/basic_chat/chat_server +#Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/chat_client.go b/chat_client.go new file mode 100644 index 0000000..a3e5fc3 --- /dev/null +++ b/chat_client.go @@ -0,0 +1,441 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/json" + "fmt" + "strings" + "io" + "net" + "os" + "time" + + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/gtk" + "gopkg.in/yaml.v2" + "github.com/yang3yen/xxtea-go/xxtea" +) + +// Copyright (c) 2025 Robert Strutts, License MIT + +type Config struct { + Window struct { + Title string `yaml:"title"` + } `yaml:"window"` + Connection struct { + Protocol string `yaml:"protocol"` + IP string `yaml:"ip"` + Port int `yaml:"port"` + Encryption struct { + Type string `yaml:"type"` + Key string `yaml:"key"` + } `yaml:"encryption"` + } `yaml:"connection"` + User struct { + Username string `yaml:"username"` + Timezone string `yaml:"timezone"` + } `yaml:"user"` +} + +var ( + config Config + key []byte +) + +type Message struct { + Username string + Text string + Time string +} + +type Encryptor interface { + Encrypt([]byte) ([]byte, error) + Decrypt([]byte) ([]byte, error) +} + +type AESEncryptor struct { + key []byte +} + +type XXTEAEncryptor struct { + key []byte +} + +func (a *AESEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func (a *AESEncryptor) Decrypt(ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} + +func (x *XXTEAEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + result, err := xxtea.Encrypt(plaintext, x.key, true, 0) + if err != nil { + return nil, err + } + return result, nil +} + +func (x *XXTEAEncryptor) Decrypt(ciphertext []byte) ([]byte, error) { + result, err := xxtea.Decrypt(ciphertext, x.key, true, 0) + if err != nil { + return nil, err + } + return result, nil +} + +var encryptor Encryptor + +func loadConfig(filename string) (*Config, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading config file: %v", err) + } + + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, fmt.Errorf("error parsing config file: %v", err) + } + + key = []byte(config.Connection.Encryption.Key) + + // Initialize the appropriate encryptor + switch config.Connection.Encryption.Type { + case "aes": + // AES requires exactly 32 bytes key + if len(key) != 32 { + return nil, fmt.Errorf("AES key must be exactly 32 bytes") + } + encryptor = &AESEncryptor{key: key} + case "xxtea": + // XXTEA Recommended 16 bytes key + if len(key) < 16 { + return nil, fmt.Errorf("XXTEA key should be at least 16 bytes") + } + key := key[:16] // Take the first 16 bytes + encryptor = &XXTEAEncryptor{key: key} + default: + return nil, fmt.Errorf("unsupported encryption type: %s", config.Connection.Encryption.Type) + } + + return &config, nil +} + +func setupTreeView(treeView *gtk.TreeView) *gtk.ListStore { + renderer, _ := gtk.CellRendererTextNew() + + columns := []struct { + Title string + ID int + }{ + {"Username", 0}, + {"Message", 1}, + {"Time", 2}, + } + + for _, col := range columns { + column, _ := gtk.TreeViewColumnNew() + column.SetTitle(col.Title) + column.PackStart(renderer, true) + column.AddAttribute(renderer, "text", col.ID) + treeView.AppendColumn(column) + } + + listStore, _ := gtk.ListStoreNew(glib.TYPE_STRING, glib.TYPE_STRING, glib.TYPE_STRING) + treeView.SetModel(listStore) + + return listStore +} + +func scrollToBottom(scrolledWindow *gtk.ScrolledWindow, treeView *gtk.TreeView) { + // Get the vertical adjustment + adjustment := scrolledWindow.GetVAdjustment() + + // Get the last path in the tree view + model, err := treeView.GetModel() + if err != nil { + fmt.Printf("Error getting model: %v\n", err) + return + } + + listStore, ok := model.(*gtk.ListStore) + if !ok { + fmt.Printf("Error: model is not a ListStore\n") + return + } + + // Get first iterator + iter, valid := listStore.GetIterFirst() + if !valid { + return + } + + var lastPath *gtk.TreePath + for { + path, err := listStore.GetPath(iter) + if err != nil { + break + } + lastPath = path + + if !listStore.IterNext(iter) { + break + } + } + + if lastPath != nil { + // First scroll to make sure the last row is in view + treeView.ScrollToCell(lastPath, nil, true, 1.0, 1.0) + + // Process events to ensure scroll takes effect + for gtk.EventsPending() { + gtk.MainIterationDo(false) + } + + // Force the adjustment to the maximum value + upper := adjustment.GetUpper() + pageSize := adjustment.GetPageSize() + + // Set value to maximum possible + adjustment.SetValue(upper - pageSize) + + // Force another GUI update + for gtk.EventsPending() { + gtk.MainIterationDo(false) + } + + // Set the value one more time to ensure it sticks + adjustment.SetValue(upper - pageSize) + } +} + +func wrapText(text string, lineWidth int) string { + words := strings.Fields(text) // Split the text into words + var result strings.Builder + var line string + + for _, word := range words { + if len(line)+len(word)+1 > lineWidth { + result.WriteString(line + "\n") + line = word + } else { + if line != "" { + line += " " + } + line += word + } + } + + if line != "" { + result.WriteString(line) + } + + return result.String() +} + +func say(conn net.Conn, text string, username string, timezone *time.Location) { + msg := Message{ + Username: username, + Text: text, + Time: time.Now().In(timezone).Format("2006-01-02 03:04:05 PM"), + } + + jsonMsg, _ := json.Marshal(msg) + encrypted, err := encryptor.Encrypt(jsonMsg) + if err != nil { + fmt.Printf("Encryption error: %v\n", err) + return + } + conn.Write(encrypted) +} + +func showErrorDialog(parent *gtk.Window, message string) { + // Create a new message dialog + dialog := gtk.MessageDialogNew( + parent, // Parent window + gtk.DIALOG_MODAL, // Make the dialog modal + gtk.MESSAGE_ERROR, // Message type (error) + gtk.BUTTONS_OK, // Buttons to show ("OK") + message, // Message text + ) + + // Run the dialog and wait for the user to close it + dialog.Run() + dialog.Destroy() // Clean up the dialog +} + +func main() { + // Load configuration + config, err := loadConfig("chat_client.yaml") + if err != nil { + fmt.Println("Error loading config:", err) + os.Exit(1) + } + + // Load timezone + timezone, err := time.LoadLocation(config.User.Timezone) + if err != nil { + fmt.Println("Error loading timezone:", err) + timezone = time.Local // Fallback to local time if timezone loading fails + } + + gtk.Init(nil) + + builder, _ := gtk.BuilderNew() + builder.AddFromString(` + + + ` + config.Window.Title + ` + 600 + 400 + + + vertical + + + true + true + + + true + true + + + + + + + + + + + + + + `) + + windowObj, _ := builder.GetObject("window") + window := windowObj.(*gtk.Window) + window.Connect("destroy", gtk.MainQuit) + + treeViewObj, _ := builder.GetObject("treeview") + treeView := treeViewObj.(*gtk.TreeView) + listStore := setupTreeView(treeView) + + scrolledObj, _ := builder.GetObject("scrolled") + scrolledWindow := scrolledObj.(*gtk.ScrolledWindow) + + entryObj, _ := builder.GetObject("entry") + entry := entryObj.(*gtk.Entry) + + // Connect the "destroy" signal to quit the application + window.Connect("destroy", func() { + fmt.Println("Window closed") + gtk.MainQuit() + }) + + // Setup connection + addr := fmt.Sprintf("%s:%d", config.Connection.IP, config.Connection.Port) + var conn net.Conn + var connErr error + + if config.Connection.Protocol == "tcp" { + conn, connErr = net.Dial("tcp", addr) + } else { + udpAddr, _ := net.ResolveUDPAddr("udp", addr) + conn, connErr = net.DialUDP("udp", nil, udpAddr) + } + + if connErr != nil { + fmt.Printf("Connection error (%s://%s): %v\n", config.Connection.Protocol, addr, connErr) + showErrorDialog(window, "Unable to connect to the server. Please check the server and try again.") + os.Exit(1) + } + defer conn.Close() + say(conn, "Welcome, User(On-line)", config.User.Username, timezone) + + builder.ConnectSignals(map[string]interface{}{ + "send_message": func() { + text, _ := entry.GetText() + entry.SetText("") + say(conn, text, config.User.Username, timezone) + + currentTime := time.Now().In(timezone).Format("2006-01-02 03:04:05 PM") + iter := listStore.Append() + err := listStore.Set(iter, + []int{0, 1, 2}, + []interface{}{"@You-said", wrapText(text, 45), currentTime}, + ) + if err != nil { + fmt.Println("Failed to set row values:", err) + } + scrollToBottom(scrolledWindow, treeView) + + }, + }) + + go func() { + for { + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil { + break + } + + decrypted, err := encryptor.Decrypt(buf[:n]) + if err != nil { + continue + } + + var msg Message + json.Unmarshal(decrypted, &msg) + + glib.IdleAdd(func() { + iter := listStore.Append() + err := listStore.Set(iter, + []int{0, 1, 2}, + []interface{}{msg.Username, wrapText(msg.Text, 45), msg.Time}, + ) + if err != nil { + fmt.Println("Failed to set row values:", err) + } + scrollToBottom(scrolledWindow, treeView) + + window.Deiconify() // UnMinimize -- nope, doesn't always do that... + window.Present() // Popup on New MSG! + }) + } + }() + + window.ShowAll() + gtk.Main() + say(conn, "Good Bye, User(Off-Line)", config.User.Username, timezone) +} diff --git a/chat_server.go b/chat_server.go new file mode 100644 index 0000000..0047b2b --- /dev/null +++ b/chat_server.go @@ -0,0 +1,441 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/json" + "fmt" + "io" + "net" + "sync" + "time" + + "gopkg.in/yaml.v2" + "os" + "github.com/yang3yen/xxtea-go/xxtea" +) + +// Copyright (c) 2025 Robert Strutts, License MIT + +// Config represents the server configuration +type Config struct { + Server struct { + Timezone string `yaml:"timezone"` + Address string `yaml:"address"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + Encryption struct { + Type string `yaml:"type"` + Key string `yaml:"key"` + } `yaml:"encryption"` + } `yaml:"server"` +} + +type ClientTCP struct { + conn net.Conn + username string +} +type ClientUDP struct { + addr *net.UDPAddr // Use UDPAddr for UDP clients + username string +} + +type UserManagerTCP struct { + clients map[net.Conn]*ClientTCP + mutex sync.Mutex +} +type UserManagerUDP struct { + clients map[string]*ClientUDP // Use a string key (e.g., addr.String()) + mutex sync.Mutex +} + +func NewUserManagerTCP() *UserManagerTCP { + return &UserManagerTCP{ + clients: make(map[net.Conn]*ClientTCP), + } +} +func NewUserManagerUDP() *UserManagerUDP { + return &UserManagerUDP{ + clients: make(map[string]*ClientUDP), + } +} + +func (um *UserManagerTCP) AddClientTCP(conn net.Conn, username string) { + um.mutex.Lock() + defer um.mutex.Unlock() + um.clients[conn] = &ClientTCP{conn: conn, username: username} +} +func (um *UserManagerUDP) AddClientUDP(addr *net.UDPAddr, username string) { + um.mutex.Lock() + defer um.mutex.Unlock() + um.clients[addr.String()] = &ClientUDP{addr: addr, username: username} +} + + +func (um *UserManagerTCP) RemoveClientTCP(conn net.Conn) { + um.mutex.Lock() + defer um.mutex.Unlock() + delete(um.clients, conn) +} +func (um *UserManagerUDP) RemoveClientUDP(addr *net.UDPAddr) { + um.mutex.Lock() + defer um.mutex.Unlock() + delete(um.clients, addr.String()) +} + +func (um *UserManagerTCP) GetUserListTCP() string { + um.mutex.Lock() + defer um.mutex.Unlock() + var userList string + for _, client := range um.clients { + userList += client.username + ", " + } + return userList +} +func (um *UserManagerUDP) GetUserListUDP() string { + um.mutex.Lock() + defer um.mutex.Unlock() + var userList string + for _, client := range um.clients { + userList += client.username + ", " + } + return userList +} + +var ( + userManagerTCP *UserManagerTCP + userManagerUDP *UserManagerUDP + config Config + key []byte +) + +type Message struct { + Username string + Text string + Time string +} + +type Encryptor interface { + Encrypt([]byte) ([]byte, error) + Decrypt([]byte) ([]byte, error) +} + +type AESEncryptor struct { + key []byte +} + +type XXTEAEncryptor struct { + key []byte +} + +func (a *AESEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func (a *AESEncryptor) Decrypt(ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(a.key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonceSize := gcm.NonceSize() + if len(ciphertext) < nonceSize { + return nil, fmt.Errorf("ciphertext too short") + } + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + return gcm.Open(nil, nonce, ciphertext, nil) +} + +func (x *XXTEAEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + result, err := xxtea.Encrypt(plaintext, x.key, true, 0) + if err != nil { + return nil, err + } + return result, nil +} + +func (x *XXTEAEncryptor) Decrypt(ciphertext []byte) ([]byte, error) { + result, err := xxtea.Decrypt(ciphertext, x.key, true, 0) + if err != nil { + return nil, err + } + return result, nil +} + +var encryptor Encryptor + +func loadConfig(filename string) error { + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("error reading config file: %v", err) + } + + err = yaml.Unmarshal(data, &config) + if err != nil { + return fmt.Errorf("error parsing config file: %v", err) + } + + key = []byte(config.Server.Encryption.Key) + + // Initialize the appropriate encryptor + switch config.Server.Encryption.Type { + case "aes": + // AES requires exactly 32 bytes key + if len(key) != 32 { + return fmt.Errorf("AES key must be exactly 32 bytes") + } + encryptor = &AESEncryptor{key: key} + case "xxtea": + // XXTEA Recommended 16 bytes key + if len(key) < 16 { + return fmt.Errorf("XXTEA key should be at least 16 bytes") + } + key := key[:16] // Take the first 16 bytes + encryptor = &XXTEAEncryptor{key: key} + default: + return fmt.Errorf("unsupported encryption type: %s", config.Server.Encryption.Type) + } + + return nil +} + +func sayTCP(conn net.Conn, text string) { + encrypted, err := sayServer(text) + if err != nil { + return + } + conn.Write(encrypted) +} +func sayUDP(conn *net.UDPConn, addr *net.UDPAddr, text string) { + encrypted, err := sayServer(text) + if err != nil { + return + } + conn.WriteToUDP(encrypted, addr) +} + +func sayServer(text string) ([]byte, error) { + // Load timezone + timezone, err := time.LoadLocation(config.Server.Timezone) + if err != nil { + fmt.Println("Error loading timezone:", err) + timezone = time.Local // Fallback to local time if timezone loading fails + } + + msg := Message{ + Username: "@SERVER", + Text: text, + Time: time.Now().In(timezone).Format("2006-01-02 03:04:05 PM"), + } + + jsonMsg, _ := json.Marshal(msg) + encrypted, err := encryptor.Encrypt(jsonMsg) + if err != nil { + fmt.Printf("Encryption error: %v\n", err) + return nil, err + } + return encrypted, nil +} + +func handleTCPClient(conn net.Conn) { + defer conn.Close() + + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil { + return + } + decrypted, err := encryptor.Decrypt(buf[:n]) + if err != nil { + return + } + + var msg Message + json.Unmarshal(decrypted, &msg) + username := msg.Username + + // Add the client to the user manager + userManagerTCP.AddClientTCP(conn, username) + fmt.Printf("%s connected\n", username) + + // Say Hello to all, but self + broadcastTCP(decrypted, conn) + + for { + buf := make([]byte, 1024) + n, err := conn.Read(buf) + if err != nil { + break + } + decrypted, err := encryptor.Decrypt(buf[:n]) + if err != nil { + continue + } + + var msg Message + json.Unmarshal(decrypted, &msg) + + message := string(msg.Text) + if message == "users!" { + // Handle the 'users' command + userList := userManagerTCP.GetUserListTCP() + sayTCP(conn, "User's Online: "+userList) + } else { + // Broadcast the message to all other clients + broadcastTCP(decrypted, conn) + } + } + + // Remove the client from the user manager when they disconnect + userManagerTCP.RemoveClientTCP(conn) + fmt.Printf("%s disconnected\n", username) +} + +func startTCPServer() error { + address := fmt.Sprintf("%s:%d", config.Server.Address, config.Server.Port) + ln, err := net.Listen("tcp", address) + if err != nil { + return fmt.Errorf("error starting TCP server: %v", err) + } + defer ln.Close() + + fmt.Printf("TCP server listening on %s using %s encryption\n", + address, config.Server.Encryption.Type) + + for { + conn, err := ln.Accept() + if err != nil { + fmt.Printf("Error accepting connection: %v\n", err) + continue + } + go handleTCPClient(conn) + } +} + +func startUDPServer() error { + address := fmt.Sprintf("%s:%d", config.Server.Address, config.Server.Port) + addr, err := net.ResolveUDPAddr("udp", address) + if err != nil { + return fmt.Errorf("error resolving UDP address: %v", err) + } + + conn, err := net.ListenUDP("udp", addr) + if err != nil { + return fmt.Errorf("error starting UDP server: %v", err) + } + defer conn.Close() + + fmt.Printf("UDP server listening on %s using %s encryption\n", + address, config.Server.Encryption.Type) + + for { + buf := make([]byte, 1024) + n, addr, err := conn.ReadFromUDP(buf) + if err != nil { + fmt.Printf("Error reading UDP: %v\n", err) + continue + } + + decrypted, err := encryptor.Decrypt(buf[:n]) + if err != nil { + fmt.Printf("Decryption error: %v\n", err) + continue + } + + var msg Message + if err := json.Unmarshal(decrypted, &msg); err != nil { + fmt.Printf("Error unmarshalling message: %v\n", err) + continue + } + + // Add the client to the user manager if they're new + if _, exists := userManagerUDP.clients[addr.String()]; !exists { + userManagerUDP.AddClientUDP(addr, msg.Username) + fmt.Printf("%s connected from %s\n", msg.Username, addr.String()) + } + + message := msg.Text + if message == "users!" { + // Handle the 'users' command + userList := userManagerUDP.GetUserListUDP() + sayUDP(conn, addr, "User's Online: "+userList) + } else { + // Broadcast the message to all other clients + broadcastUDP(conn, decrypted, addr) + } + } +} + +func broadcastTCP(data []byte, sender net.Conn) { + encrypted, err := encryptor.Encrypt(data) + if err != nil { + fmt.Printf("Encryption error: %v\n", err) + return + } + + userManagerTCP.mutex.Lock() + defer userManagerTCP.mutex.Unlock() + for client := range userManagerTCP.clients { + if client != sender { + client.Write(encrypted) + } + } +} + +func broadcastUDP(conn *net.UDPConn, data []byte, sender *net.UDPAddr) { + encrypted, err := encryptor.Encrypt(data) + if err != nil { + fmt.Printf("Encryption error: %v\n", err) + return + } + + userManagerUDP.mutex.Lock() + defer userManagerUDP.mutex.Unlock() + for _, client := range userManagerUDP.clients { + if client.addr.String() != sender.String() { + conn.WriteToUDP(encrypted, client.addr) + } + } +} + +func main() { + if err := loadConfig("chat_server.yaml"); err != nil { + fmt.Printf("Failed to load configuration: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Starting %s server...\n", config.Server.Protocol) + + var err error + if config.Server.Protocol == "tcp" { + userManagerTCP = NewUserManagerTCP() + err = startTCPServer() + } else if config.Server.Protocol == "udp" { + userManagerUDP = NewUserManagerUDP() + err = startUDPServer() + } else { + fmt.Printf("Invalid protocol specified: %s\n", config.Server.Protocol) + os.Exit(1) + } + + if err != nil { + fmt.Printf("Server error: %v\n", err) + os.Exit(1) + } +} diff --git a/client-example.yaml b/client-example.yaml new file mode 100644 index 0000000..ddf9ba7 --- /dev/null +++ b/client-example.yaml @@ -0,0 +1,12 @@ +window: + title: "Basic Chatbox" +connection: + protocol: "tcp" # tcp or udp + ip: "127.0.0.1" # Change to the IP Address of the Chat Server!! + port: 8080 + encryption: + type: "aes" # aes or xxtea + key: "eb9fcf2902a7177c2f6c693bcaada13c" # Must be 32 bytes long +user: + username: "Rick James" + timezone: "America/Chicago" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9a4c180 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module basic_chat + +go 1.23.2 + +require github.com/gotk3/gotk3 v0.6.5-0.20240618185848-ff349ae13f56 + +require ( + github.com/yang3yen/xxtea-go v1.0.3 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/make_a_key.go b/make_a_key.go new file mode 100644 index 0000000..30bdce3 --- /dev/null +++ b/make_a_key.go @@ -0,0 +1,19 @@ +package main + +import ( + "crypto/rand" + "fmt" +) + +func main() { + // Generate a secure random key + key := make([]byte, 16) + _, err := rand.Read(key) + if err != nil { + fmt.Println("Error generating random key:", err) + return + } + + // Print the raw 32-byte key as a hexadecimal string + fmt.Printf("32-byte key (hex): %x\n", key) +} diff --git a/server-example.yaml b/server-example.yaml new file mode 100644 index 0000000..0c698af --- /dev/null +++ b/server-example.yaml @@ -0,0 +1,9 @@ +server: + timezone: "America/Chicago" + address: "0.0.0.0" # 0.0.0.0 Bind & Listen on all interfaces!! + port: 8080 + protocol: "tcp" # tcp or udp + encryption: + type: "aes" # aes or xxtea + key: "eb9fcf2902a7177c2f6c693bcaada13c" # Must be 32 bytes long +# Key must match the Client's as well! Run make_a_key to get a good new key...