Robert 10 months ago
commit d47db7ec70
  1. 6
      .gitignore
  2. 9
      BasicChat.desktop
  3. 50
      INSTALL
  4. 22
      LICENSE
  5. 7
      README
  6. BIN
      basic_chat.png
  7. 11
      basic_chat_server.service
  8. 441
      chat_client.go
  9. 441
      chat_server.go
  10. 12
      client-example.yaml
  11. 10
      go.mod
  12. 19
      make_a_key.go
  13. 9
      server-example.yaml

6
.gitignore vendored

@ -0,0 +1,6 @@
chat_client.yaml
chat_server.yaml
chat_client
chat_server
make_a_key
go.sum

@ -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

@ -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

@ -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.

@ -0,0 +1,7 @@
# Basic Chat
Author: By Robert Strutts
## License MIT - Copyright 2025 - Robert Strutts
# View INSTALL guide for help
```
cat INSTALL
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -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

@ -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(`
<interface>
<object class="GtkWindow" id="window">
<property name="title">` + config.Window.Title + `</property>
<property name="default-width">600</property>
<property name="default-height">400</property>
<child>
<object class="GtkBox" id="box">
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="scrolled">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
<child>
<object class="GtkTreeView" id="treeview">
<property name="vexpand">true</property>
<property name="hexpand">true</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkEntry" id="entry">
<signal name="activate" handler="send_message"/>
</object>
</child>
</object>
</child>
</object>
</interface>
`)
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)
}

@ -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)
}
}

@ -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"

@ -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
)

@ -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)
}

@ -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...
Loading…
Cancel
Save