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