387 lines
8.5 KiB
Go
387 lines
8.5 KiB
Go
package main
|
|
|
|
// HTTP handlers and hub run loop integration for RadChat.
|
|
//
|
|
// This file contains:
|
|
// - WebSocket and HTTP endpoints (username check, user count, file upload/download)
|
|
// - Helpers to send messages to specific clients and broadcast user lists
|
|
// - The hub Run loop that manages registration, unregistration, and broadcasting
|
|
//
|
|
// The actual Hub/Client definitions live in the server package. Here we glue
|
|
// them to HTTP.
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"radchat/server"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// IsUsernameTaken returns true if the given username already exists among connected clients (case-insensitive).
|
|
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
|
|
}
|
|
|
|
// HandleUserCountCheck responds with the current number of connected users.
|
|
func HandleUserCountCheck(hub *server.Hub, w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
response := map[string]any{
|
|
"userCount": int64(len(hub.Clients)),
|
|
}
|
|
|
|
err := json.NewEncoder(w).Encode(response)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// HandleUsernameCheck validates a requested username and checks for collisions.
|
|
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 HandleFileUpload(h *server.Hub, filesDir string, fileTimeout time.Duration, w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
err := r.ParseMultipartForm(256 << 20)
|
|
if err != nil {
|
|
http.Error(w, "Error parsing form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get all files
|
|
files := r.MultipartForm.File
|
|
|
|
_ = r.ParseForm()
|
|
username := r.FormValue("username")
|
|
clientId := r.FormValue("client_id")
|
|
|
|
var uploadedFiles []string
|
|
|
|
for _, fileHeaders := range files {
|
|
for _, fileHeader := range fileHeaders {
|
|
file, err := fileHeader.Open()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fileBytes, err := io.ReadAll(file)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
continue
|
|
}
|
|
|
|
filename := SanitizeFilename(fileHeader.Filename)
|
|
|
|
// get filename before the extension
|
|
|
|
fileExt := filepath.Ext(filename)
|
|
fileName := strings.TrimSuffix(filename, fileExt)
|
|
fileFullName := fileName + "_" + time.Now().Format("20060102150405") + fileExt
|
|
|
|
filePath := filepath.Join(filesDir, fileFullName)
|
|
|
|
err = os.WriteFile(filePath, fileBytes, 0644)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
continue
|
|
}
|
|
|
|
uploadedFiles = append(uploadedFiles, fileFullName)
|
|
|
|
fileFullName = url.PathEscape(fileFullName)
|
|
fileLocation := filepath.Join(r.URL.Path, fileFullName)
|
|
|
|
scheme := GetScheme(r)
|
|
|
|
fileLink := scheme + "://" + filepath.Join(r.Host, fileLocation)
|
|
|
|
// find our client and send a message as them
|
|
clients := h.Clients
|
|
for client := range clients {
|
|
if client.Id == clientId {
|
|
broadcastMsg := Message{
|
|
Type: "chat_message",
|
|
Username: username,
|
|
Data: map[string]any{
|
|
"message": fileLink,
|
|
"timestamp": time.Now().UnixMilli(),
|
|
},
|
|
}
|
|
data, _ := json.Marshal(broadcastMsg)
|
|
h.Broadcast <- data
|
|
break
|
|
}
|
|
}
|
|
|
|
go func() {
|
|
time.Sleep(fileTimeout)
|
|
err := os.Remove(filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
_ = file.Close()
|
|
}
|
|
}
|
|
|
|
// Respond with success
|
|
response := map[string]interface{}{
|
|
"success": true,
|
|
"uploaded_files": uploadedFiles,
|
|
"count": len(uploadedFiles),
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
err = json.NewEncoder(w).Encode(response)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func HandleFileDownload(filesDir string, w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
segments := strings.Split(path, "/")
|
|
|
|
if len(segments) < 2 {
|
|
http.Error(w, "Filename required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filename := segments[len(segments)-1]
|
|
|
|
filename = SanitizeFilename(filename)
|
|
if filename == "" {
|
|
http.Error(w, "Invalid filename", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
filePath := filepath.Join(filesDir, filename)
|
|
|
|
absUploadsDir, err := filepath.Abs(filesDir)
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
absFilePath, err := filepath.Abs(filePath)
|
|
if err != nil {
|
|
http.Error(w, "Invalid file path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(absFilePath, absUploadsDir) {
|
|
http.Error(w, "Access denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
|
http.Error(w, "File not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
http.ServeFile(w, r, filePath)
|
|
}
|
|
|
|
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:
|
|
var messageJson []byte
|
|
messageJson, _ = json.Marshal(Message{
|
|
Type: "system_message",
|
|
Data: fmt.Sprintf("%s left the voice chat", client.Username),
|
|
DataType: "system_message_leave",
|
|
// FIX: Please...
|
|
// Using CONFIG.APP.SYSTEM_MSG_DEFAULT_TIMEOUT from javascript config
|
|
// Hard coding it here for now
|
|
DataTime: 5000,
|
|
})
|
|
|
|
for cli := range client.Hub.Clients {
|
|
SendToClient(client.Hub, cli.Id, messageJson)
|
|
}
|
|
|
|
h.Mutex.Lock()
|
|
if _, ok := h.Clients[client]; ok {
|
|
delete(h.Clients, client)
|
|
close(client.Send)
|
|
}
|
|
h.Mutex.Unlock()
|
|
|
|
log.Println("Client left. Client ID:", client.Id)
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|