From d47db7ec70fe8cd2c2c352f2576c0fc06ddd9e26 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 8 Feb 2025 20:16:39 -0500 Subject: [PATCH] init --- .gitignore | 6 + BasicChat.desktop | 9 + INSTALL | 50 +++++ LICENSE | 22 ++ README | 7 + basic_chat.png | Bin 0 -> 4288 bytes basic_chat_server.service | 11 + chat_client.go | 441 ++++++++++++++++++++++++++++++++++++++ chat_server.go | 441 ++++++++++++++++++++++++++++++++++++++ client-example.yaml | 12 ++ go.mod | 10 + make_a_key.go | 19 ++ server-example.yaml | 9 + 13 files changed, 1037 insertions(+) create mode 100644 .gitignore create mode 100755 BasicChat.desktop create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 README create mode 100644 basic_chat.png create mode 100644 basic_chat_server.service create mode 100644 chat_client.go create mode 100644 chat_server.go create mode 100644 client-example.yaml create mode 100644 go.mod create mode 100644 make_a_key.go create mode 100644 server-example.yaml 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 0000000000000000000000000000000000000000..63b9ad2e9ac391b0930e68e7111bfff6a2c3824f GIT binary patch literal 4288 zcmV;x5I^sUP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 iR4_9-H8wglH7hVMIxsM_s 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...