backend tidying, add cli options
This commit is contained in:
parent
f6e2511afb
commit
9350e27644
1
.idea/dictionaries/project.xml
generated
1
.idea/dictionaries/project.xml
generated
@ -2,6 +2,7 @@
|
||||
<dictionary name="project">
|
||||
<words>
|
||||
<w>abcdefghijklmnopqrstuvwxyz</w>
|
||||
<w>bufsize</w>
|
||||
<w>omitempty</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
202
client.go
Normal file
202
client.go
Normal 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
180
hub.go
Normal 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
567
main.go
@ -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
18
message.go
Normal 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
13
server/client.go
Normal 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
26
server/hub.go
Normal 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
52
server/middleware.go
Normal 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
26
server/utils.go
Normal 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
47
utils.go
Normal 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]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user