backend tidying, add cli options

This commit is contained in:
Radon 2025-08-29 13:24:08 -05:00
parent f6e2511afb
commit 9350e27644
10 changed files with 586 additions and 546 deletions

View File

@ -2,6 +2,7 @@
<dictionary name="project">
<words>
<w>abcdefghijklmnopqrstuvwxyz</w>
<w>bufsize</w>
<w>omitempty</w>
</words>
</dictionary>

202
client.go Normal file
View File

@ -0,0 +1,202 @@
package main
import (
"encoding/json"
"fmt"
"radchat/server"
"time"
"github.com/gorilla/websocket"
)
func ReadPump(c *server.Client) {
defer func() {
c.Hub.Unregister <- c
err := c.Conn.Close()
if err != nil {
return
}
}()
for {
var msg Message
err := c.Conn.ReadJSON(&msg)
if err != nil {
break
}
switch msg.Type {
case "system_message":
messageContent := msg.Data.(string)
messageQualifier := msg.DataExt.(string)
messageUserList := make(map[string]bool)
for _, v := range msg.DataList {
if v == nil {
continue
}
messageUserList[v.(string)] = true
}
messageType := msg.DataType
messageTimeout := msg.DataTime
var messageJson []byte
if messageTimeout == 0 {
messageJson, _ = json.Marshal(Message{
Type: "system_message",
Data: messageContent,
DataType: messageType,
})
} else {
messageJson, _ = json.Marshal(Message{
Type: "system_message",
Data: messageContent,
DataType: messageType,
DataTime: messageTimeout,
})
}
switch messageQualifier {
case "all":
for cli := range c.Hub.Clients {
SendToClient(c.Hub, cli.Id, messageJson)
}
case "include":
for cli := range c.Hub.Clients {
if _, ok := messageUserList[cli.Id]; ok {
SendToClient(c.Hub, cli.Id, messageJson)
}
}
case "except":
for cli := range c.Hub.Clients {
if _, ok := messageUserList[cli.Id]; !ok {
SendToClient(c.Hub, cli.Id, messageJson)
}
}
default:
fmt.Println("No handler exists for system_message qualifier:", messageQualifier)
}
case "set_username":
if username, ok := msg.Data.(string); ok {
//log.Printf("Username request for client %s: %s", c.id, username)
if IsUsernameTaken(c.Hub, username) {
errorMsg := Message{
Type: "username_error",
Error: "Username is already taken. Please choose a different username.",
}
data, _ := json.Marshal(errorMsg)
c.Send <- data
continue
}
if !IsUsernameValid(username) {
errorMsg := Message{
Type: "username_error",
Error: "Invalid username. Please use 3-24 characters, letters, numbers, underscores, or hyphens.",
}
data, _ := json.Marshal(errorMsg)
c.Send <- data
continue
}
c.Username = username
SendUsersList(c.Hub)
} else {
//log.Printf("Invalid username data type: %T", msg.Data)
}
case "webrtc_offer":
// Forward WebRTC offer to target client
data, _ := json.Marshal(Message{
Type: "webrtc_offer",
UserID: c.Id,
Username: c.Username,
Offer: msg.Offer,
})
SendToClient(c.Hub, msg.Target, data)
case "webrtc_answer":
// Forward WebRTC answer to target client
data, _ := json.Marshal(Message{
Type: "webrtc_answer",
UserID: c.Id,
Answer: msg.Answer,
})
SendToClient(c.Hub, msg.Target, data)
case "webrtc_ice":
// Forward ICE candidate to target client
data, _ := json.Marshal(Message{
Type: "webrtc_ice",
UserID: c.Id,
ICE: msg.ICE,
})
SendToClient(c.Hub, msg.Target, data)
case "speaking":
// Broadcast speaking status
broadcastMsg := Message{
Type: "user_speaking",
UserID: c.Id,
Username: c.Username,
Data: msg.Data,
}
data, _ := json.Marshal(broadcastMsg)
c.Hub.Broadcast <- data
case "chat_message":
// Broadcast chat message to all users
if msg.Username != "" {
// Extract message content from Data field
var chatMsg string
if msgData, ok := msg.Data.(map[string]any); ok {
if message, exists := msgData["message"]; exists {
chatMsg, _ = message.(string)
}
}
// Use timestamp from message or current time
timestamp := msg.Timestamp
if timestamp == 0 {
timestamp = time.Now().UnixMilli()
}
if chatMsg != "" {
//log.Printf("Chat message from %s (%s): %s", msg.Username, c.id, chatMsg)
broadcastMsg := Message{
Type: "chat_message",
Username: msg.Username,
Data: map[string]any{
"message": chatMsg,
"timestamp": timestamp,
},
}
data, _ := json.Marshal(broadcastMsg)
c.Hub.Broadcast <- data
} else {
//log.Printf("Empty chat message from client %s", c.id)
}
} else {
//log.Printf("Invalid chat message format from client %s", c.id)
}
}
}
}
func WritePump(c *server.Client) {
defer func(conn *websocket.Conn) {
_ = conn.Close()
}(c.Conn)
for message := range c.Send {
err := c.Conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
return
}
}
}

180
hub.go Normal file
View File

@ -0,0 +1,180 @@
package main
import (
"encoding/json"
"log"
"net/http"
"radchat/server"
)
func IsUsernameTaken(h *server.Hub, username string) bool {
h.Mutex.RLock()
defer h.Mutex.RUnlock()
for client := range h.Clients {
if client.Username == username {
return true
}
}
return false
}
func HandleUsernameCheck(hub *server.Hub, w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var request struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
// Check if username is taken
errorString := ""
isTaken := IsUsernameTaken(hub, request.Username)
if isTaken {
errorString = "Username is already taken. Please choose a different username."
}
isValid := IsUsernameValid(request.Username)
if !isValid {
errorString = "Invalid username. Please use 3-24 characters, letters, numbers, underscores, or hyphens."
}
response := map[string]any{
"available": isValid && !isTaken,
"username": request.Username,
"error": errorString,
}
err := json.NewEncoder(w).Encode(response)
if err != nil {
return
}
}
func SendUsersList(h *server.Hub) {
h.Mutex.RLock()
users := make([]map[string]string, 0)
for client := range h.Clients {
if client.Username != "" {
users = append(users, map[string]string{
"id": client.Id,
"username": client.Username,
})
}
}
h.Mutex.RUnlock()
msg := Message{
Type: "users_list",
Data: users,
}
data, err := json.Marshal(msg)
if err != nil {
return
}
select {
case h.Broadcast <- data:
default:
}
}
func SendToClient(h *server.Hub, targetID string, message []byte) {
h.Mutex.RLock()
defer h.Mutex.RUnlock()
for client := range h.Clients {
if client.Id == targetID {
select {
case client.Send <- message:
default:
close(client.Send)
delete(h.Clients, client)
}
return
}
}
}
func HandleWebSocket(hub *server.Hub, w http.ResponseWriter, r *http.Request) {
conn, err := hub.Upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("WebSocket upgrade error:", err)
return
}
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
clientID = GenerateId()
log.Println("Client joined. Generated new client ID:", clientID)
} else {
log.Println("Client joined. Used existing client ID:", clientID)
}
client := &server.Client{
Conn: conn,
Id: clientID,
Hub: hub,
Send: make(chan []byte, 256),
}
userIdMsg := map[string]string{
"type": "client_id",
"clientId": clientID,
}
if msgBytes, err := json.Marshal(userIdMsg); err == nil {
client.Send <- msgBytes
}
client.Hub.Register <- client
go WritePump(client)
go ReadPump(client)
}
func Run(h *server.Hub) {
log.Println("Hub started and running...")
for {
select {
case client := <-h.Register:
h.Mutex.Lock()
h.Clients[client] = true
h.Mutex.Unlock()
SendUsersList(h)
case client := <-h.Unregister:
h.Mutex.Lock()
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
close(client.Send)
}
h.Mutex.Unlock()
SendUsersList(h)
case message := <-h.Broadcast:
h.Mutex.RLock()
successCount := 0
for client := range h.Clients {
select {
case client.Send <- message:
successCount++
default:
close(client.Send)
delete(h.Clients, client)
}
}
h.Mutex.RUnlock()
}
}
}

567
main.go
View File

@ -1,580 +1,55 @@
package main
import (
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"radchat/server"
)
const ChanBufferSize = 512
const CachingDisabled = true
const GzipEnabled = true
type Client struct {
conn *websocket.Conn
username string
id string
hub *Hub
send chan []byte
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mutex sync.RWMutex
}
type Message struct {
Type string `json:"type"`
Username string `json:"username,omitempty"`
UserID string `json:"userId,omitempty"`
Data any `json:"data,omitempty"`
DataExt any `json:"dataExt,omitempty"`
DataType string `json:"dataType,omitempty"`
DataTime int64 `json:"dataTime,omitempty"`
DataList []any `json:"dataList,omitempty"`
Offer any `json:"offer,omitempty"`
Answer any `json:"answer,omitempty"`
ICE any `json:"ice,omitempty"`
Target string `json:"target,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte, ChanBufferSize), // Add buffer to prevent blocking
register: make(chan *Client, ChanBufferSize),
unregister: make(chan *Client, ChanBufferSize),
}
}
func (h *Hub) run() {
log.Println("Hub started and running...")
for {
select {
case client := <-h.register:
h.mutex.Lock()
h.clients[client] = true
h.mutex.Unlock()
//log.Printf("Client registered: %s (total clients: %d)", client.id, len(h.clients))
// Send current users list to new client
h.sendUsersList()
case client := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
//log.Printf("Client unregistered: %s (total clients: %d)", client.id, len(h.clients))
}
h.mutex.Unlock()
// Send updated users list
h.sendUsersList()
case message := <-h.broadcast:
//log.Printf("Broadcasting message to %d clients: %s", len(h.clients), string(message))
h.mutex.RLock()
successCount := 0
for client := range h.clients {
select {
case client.send <- message:
successCount++
//log.Printf("Message queued for client %s", client.id)
default:
//log.Printf("Failed to queue message for client %s, closing connection", client.id)
close(client.send)
delete(h.clients, client)
}
}
h.mutex.RUnlock()
//log.Printf("Message broadcast completed. Successful sends: %d", successCount)
}
}
}
func (h *Hub) isUsernameValid(username string) bool {
minLength := 3
maxLength := 24
allowedChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
disallowedUsernames := []string{
"admin",
"user",
"guest",
"test",
"root",
"system",
"anonymous",
"default",
}
if len(username) < minLength || len(username) > maxLength {
return false
}
for _, char := range username {
if !strings.ContainsRune(allowedChars, char) {
return false
}
}
for _, disallowed := range disallowedUsernames {
if strings.EqualFold(username, disallowed) {
return false
}
}
return true
}
func (h *Hub) isUsernameTaken(username string) bool {
h.mutex.RLock()
defer h.mutex.RUnlock()
for client := range h.clients {
if client.username == username {
return true
}
}
return false
}
func (h *Hub) sendUsersList() {
h.mutex.RLock()
users := make([]map[string]string, 0)
for client := range h.clients {
if client.username != "" {
users = append(users, map[string]string{
"id": client.id,
"username": client.username,
})
}
}
h.mutex.RUnlock()
//log.Printf("sendUsersList: Found %d users with usernames", len(users))
//log.Printf("sendUsersList: Users data: %+v", users)
msg := Message{
Type: "users_list",
Data: users,
}
data, err := json.Marshal(msg)
if err != nil {
//log.Printf("sendUsersList: Error marshaling message: %v", err)
return
}
//log.Printf("sendUsersList: About to send to broadcast channel: %s", string(data))
select {
case h.broadcast <- data:
//log.Printf("sendUsersList: Message sent to broadcast channel successfully")
default:
//log.Printf("sendUsersList: WARNING - broadcast channel is full or blocked!")
}
}
func (h *Hub) sendToClient(targetID string, message []byte) {
h.mutex.RLock()
defer h.mutex.RUnlock()
for client := range h.clients {
if client.id == targetID {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
return
}
}
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
err := c.conn.Close()
if err != nil {
return
}
}()
for {
var msg Message
err := c.conn.ReadJSON(&msg)
if err != nil {
//log.Printf("Error reading message: %v", err)
break
}
switch msg.Type {
case "system_message":
messageContent := msg.Data.(string)
messageQualifier := msg.DataExt.(string)
messageUserList := make(map[string]bool)
for _, v := range msg.DataList {
if v == nil {
continue
}
messageUserList[v.(string)] = true
}
messageType := msg.DataType
messageTimeout := msg.DataTime
var messageJson []byte
if messageTimeout == 0 {
messageJson, _ = json.Marshal(Message{
Type: "system_message",
Data: messageContent,
DataType: messageType,
})
} else {
messageJson, _ = json.Marshal(Message{
Type: "system_message",
Data: messageContent,
DataType: messageType,
DataTime: messageTimeout,
})
}
switch messageQualifier {
case "all":
for cli := range c.hub.clients {
c.hub.sendToClient(cli.id, messageJson)
}
case "include":
for cli := range c.hub.clients {
if _, ok := messageUserList[cli.id]; ok {
c.hub.sendToClient(cli.id, messageJson)
}
}
case "except":
for cli := range c.hub.clients {
if _, ok := messageUserList[cli.id]; !ok {
c.hub.sendToClient(cli.id, messageJson)
}
}
default:
fmt.Println("No handler exists for system_message qualifier:", messageQualifier)
}
case "set_username":
if username, ok := msg.Data.(string); ok {
//log.Printf("Username request for client %s: %s", c.id, username)
if c.hub.isUsernameTaken(username) {
errorMsg := Message{
Type: "username_error",
Error: "Username is already taken. Please choose a different username.",
}
data, _ := json.Marshal(errorMsg)
c.send <- data
continue
}
if !c.hub.isUsernameValid(username) {
errorMsg := Message{
Type: "username_error",
Error: "Invalid username. Please use 3-24 characters, letters, numbers, underscores, or hyphens.",
}
data, _ := json.Marshal(errorMsg)
c.send <- data
continue
}
c.username = username
c.hub.sendUsersList()
} else {
//log.Printf("Invalid username data type: %T", msg.Data)
}
case "webrtc_offer":
// Forward WebRTC offer to target client
data, _ := json.Marshal(Message{
Type: "webrtc_offer",
UserID: c.id,
Username: c.username,
Offer: msg.Offer,
})
c.hub.sendToClient(msg.Target, data)
case "webrtc_answer":
// Forward WebRTC answer to target client
data, _ := json.Marshal(Message{
Type: "webrtc_answer",
UserID: c.id,
Answer: msg.Answer,
})
c.hub.sendToClient(msg.Target, data)
case "webrtc_ice":
// Forward ICE candidate to target client
data, _ := json.Marshal(Message{
Type: "webrtc_ice",
UserID: c.id,
ICE: msg.ICE,
})
c.hub.sendToClient(msg.Target, data)
case "speaking":
// Broadcast speaking status
broadcastMsg := Message{
Type: "user_speaking",
UserID: c.id,
Username: c.username,
Data: msg.Data,
}
data, _ := json.Marshal(broadcastMsg)
c.hub.broadcast <- data
case "chat_message":
// Broadcast chat message to all users
if msg.Username != "" {
// Extract message content from Data field
var chatMsg string
if msgData, ok := msg.Data.(map[string]any); ok {
if message, exists := msgData["message"]; exists {
chatMsg, _ = message.(string)
}
}
// Use timestamp from message or current time
timestamp := msg.Timestamp
if timestamp == 0 {
timestamp = time.Now().UnixMilli()
}
if chatMsg != "" {
//log.Printf("Chat message from %s (%s): %s", msg.Username, c.id, chatMsg)
broadcastMsg := Message{
Type: "chat_message",
Username: msg.Username,
Data: map[string]any{
"message": chatMsg,
"timestamp": timestamp,
},
}
data, _ := json.Marshal(broadcastMsg)
c.hub.broadcast <- data
} else {
//log.Printf("Empty chat message from client %s", c.id)
}
} else {
//log.Printf("Invalid chat message format from client %s", c.id)
}
}
}
}
func (c *Client) writePump() {
defer func(conn *websocket.Conn) {
_ = conn.Close()
}(c.conn)
for message := range c.send {
//log.Printf("Sending message to client %s: %s", c.id, string(message))
err := c.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
//log.Printf("Error sending message to client %s: %v", c.id, err)
return
}
//log.Printf("Message sent successfully to client %s", c.id)
}
}
func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
//log.Printf("WebSocket upgrade error: %v", err)
return
}
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
clientID = generateID()
log.Println("Generated new client ID:", clientID)
} else {
log.Println("Using existing client ID:", clientID)
}
client := &Client{
conn: conn,
id: clientID,
hub: hub,
send: make(chan []byte, 256),
}
userIdMsg := map[string]string{
"type": "client_id",
"clientId": clientID,
}
if msgBytes, err := json.Marshal(userIdMsg); err == nil {
client.send <- msgBytes
}
log.Println("New client connected")
client.hub.register <- client
// Important: Start both pumps as goroutines
go client.writePump()
go client.readPump()
// log.Printf("Started read and write pumps for client: %s", clientID)
}
func generateID() string {
timestamp := time.Now().UnixNano()
randomComponent := time.Now().UnixNano() % 1000000 // Add some randomness
data := fmt.Sprintf("%d-%d", timestamp, randomComponent)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])[:16]
}
func MiddlewareChain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
return func(final http.Handler) http.Handler {
for _, middleware := range middlewares {
final = middleware(final)
}
return final
}
}
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.Writer.Write(data)
}
func CacheDisableMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
next.ServeHTTP(w, r)
})
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer func(gz *gzip.Writer) {
_ = gz.Close()
}(gz)
w.Header().Set("Content-Encoding", "gzip")
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}
func handleUsernameCheck(hub *Hub, w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var request struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
// Check if username is taken
errorString := ""
isTaken := hub.isUsernameTaken(request.Username)
if isTaken {
errorString = "Username is already taken. Please choose a different username."
}
isValid := hub.isUsernameValid(request.Username)
if !isValid {
errorString = "Invalid username. Please use 3-24 characters, letters, numbers, underscores, or hyphens."
}
response := map[string]any{
"available": isValid && !isTaken,
"username": request.Username,
"error": errorString,
}
err := json.NewEncoder(w).Encode(response)
if err != nil {
return
}
}
func main() {
var ip = flag.String("ip", "", "Server address in the format ip:port (e.g., 192.168.1.1)")
var ip = flag.String("ip", "localhost", "Server address (e.g., 192.168.1.1)")
var port = flag.Int("port", 8080, "Server port")
var bufSize = flag.Int("bufsize", 256, "Channel buffer size")
var gzipEnabled = flag.Bool("gzip-enable", false, "Enable gzip compression")
var cachingDisabled = flag.Bool("cache-disable", false, "Disable caching")
var originCheck = flag.Bool("origin-check", false, "Enable origin check")
var help = flag.Bool("help", false, "Show help")
flag.Parse()
if *help {
flag.PrintDefaults()
return
}
address := fmt.Sprintf("%s:%d", *ip, *port)
hub := newHub()
go hub.run()
hub := server.NewHub(*bufSize, server.Upgrader(address, !*originCheck))
go Run(hub)
mux := http.NewServeMux()
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleWebSocket(hub, w, r)
HandleWebSocket(hub, w, r)
})
mux.HandleFunc("/check-username", func(w http.ResponseWriter, r *http.Request) {
handleUsernameCheck(hub, w, r)
HandleUsernameCheck(hub, w, r)
})
var middleware []func(http.Handler) http.Handler
if CachingDisabled {
middleware = append(middleware, CacheDisableMiddleware)
if *cachingDisabled {
middleware = append(middleware, server.CacheDisableMiddleware)
}
if GzipEnabled {
middleware = append(middleware, GzipMiddleware)
if *gzipEnabled {
middleware = append(middleware, server.GzipMiddleware)
}
fileServer := http.FileServer(http.Dir("./static/"))
mux.Handle("/", MiddlewareChain(middleware...)(fileServer))
mux.Handle("/", server.MiddlewareChain(middleware...)(fileServer))
log.Printf("Voice chat server starting on %s...\n", address)
log.Fatal(http.ListenAndServe(address, mux))

18
message.go Normal file
View File

@ -0,0 +1,18 @@
package main
type Message struct {
Type string `json:"type"`
Username string `json:"username,omitempty"`
UserID string `json:"userId,omitempty"`
Data any `json:"data,omitempty"`
DataExt any `json:"dataExt,omitempty"`
DataType string `json:"dataType,omitempty"`
DataTime int64 `json:"dataTime,omitempty"`
DataList []any `json:"dataList,omitempty"`
Offer any `json:"offer,omitempty"`
Answer any `json:"answer,omitempty"`
ICE any `json:"ice,omitempty"`
Target string `json:"target,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}

13
server/client.go Normal file
View File

@ -0,0 +1,13 @@
package server
import (
"github.com/gorilla/websocket"
)
type Client struct {
Conn *websocket.Conn
Username string
Id string
Hub *Hub
Send chan []byte
}

26
server/hub.go Normal file
View File

@ -0,0 +1,26 @@
package server
import (
"sync"
"github.com/gorilla/websocket"
)
type Hub struct {
Clients map[*Client]bool
Broadcast chan []byte
Register chan *Client
Unregister chan *Client
Mutex sync.RWMutex
Upgrader websocket.Upgrader
}
func NewHub(bufferSize int, upgrader websocket.Upgrader) *Hub {
return &Hub{
Clients: make(map[*Client]bool),
Broadcast: make(chan []byte, bufferSize),
Register: make(chan *Client, bufferSize),
Unregister: make(chan *Client, bufferSize),
Upgrader: upgrader,
}
}

52
server/middleware.go Normal file
View File

@ -0,0 +1,52 @@
package server
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
func MiddlewareChain(middlewares ...func(http.Handler) http.Handler) func(http.Handler) http.Handler {
return func(final http.Handler) http.Handler {
for _, middleware := range middlewares {
final = middleware(final)
}
return final
}
}
type gzipResponseWriter struct {
http.ResponseWriter
io.Writer
}
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
return g.Writer.Write(data)
}
func CacheDisableMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
next.ServeHTTP(w, r)
})
}
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer func(gz *gzip.Writer) {
_ = gz.Close()
}(gz)
w.Header().Set("Content-Encoding", "gzip")
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}

26
server/utils.go Normal file
View File

@ -0,0 +1,26 @@
package server
import (
"net/http"
"strings"
"github.com/gorilla/websocket"
)
func Upgrader(address string, bypass bool) websocket.Upgrader {
return websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
if bypass {
return true
}
origin := r.Header.Get("Origin")
if origin == "" {
return false
}
if strings.HasPrefix(origin, "http://"+address) || strings.HasPrefix(origin, "https://"+address) {
return true
}
return false
},
}
}

47
utils.go Normal file
View File

@ -0,0 +1,47 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
func IsUsernameValid(username string) bool {
minLength := 3
maxLength := 24
allowedChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
disallowedUsernames := []string{
"admin",
"user",
"guest",
"test",
"root",
"system",
"anonymous",
"default",
}
if len(username) < minLength || len(username) > maxLength {
return false
}
for _, char := range username {
if !strings.ContainsRune(allowedChars, char) {
return false
}
}
for _, disallowed := range disallowedUsernames {
if strings.EqualFold(username, disallowed) {
return false
}
}
return true
}
func GenerateId() string {
timestamp := time.Now().UnixNano()
randomComponent := time.Now().UnixNano() % 1000000 // Add some randomness
data := fmt.Sprintf("%d-%d", timestamp, randomComponent)
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])[:16]
}