commit
d47db7ec70
@ -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 |
||||
``` |
||||
|
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…
Reference in new issue