From 1d2c6b9e86eadbf7b385701a6800fe21a56b57c7 Mon Sep 17 00:00:00 2001 From: Gab <24553253+gabrix73@users.noreply.github.com> Date: Mon, 6 Oct 2025 04:07:26 +0200 Subject: Create noshitalk-web-client.go --- noshitalk-web-client.go | 851 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 851 insertions(+) create mode 100644 noshitalk-web-client.go (limited to 'noshitalk-web-client.go') diff --git a/noshitalk-web-client.go b/noshitalk-web-client.go new file mode 100644 index 0000000..b72b977 --- /dev/null +++ b/noshitalk-web-client.go @@ -0,0 +1,851 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "log" + "net/http" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// Message types +const ( + MsgTypeHandshake = "handshake" + MsgTypePublic = "public" + MsgTypePrivate = "private" + MsgTypeSystem = "system" + MsgTypeUserJoin = "user_join" + MsgTypeUserLeave = "user_leave" + MsgTypeUsersList = "users_list" +) + +// User represents a connected user +type User struct { + ID string + Name string + Avatar string + Fingerprint string + Conn *websocket.Conn + LastSeen time.Time + mu sync.Mutex +} + +// Message represents a chat message +type Message struct { + Type string `json:"type"` + From string `json:"from,omitempty"` + FromName string `json:"from_name,omitempty"` + FromAvatar string `json:"from_avatar,omitempty"` + To string `json:"to,omitempty"` + Text string `json:"text,omitempty"` + Timestamp time.Time `json:"timestamp,omitempty"` +} + +// UserInfo for user list +type UserInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Status string `json:"status"` +} + +// Server represents the chat server +type Server struct { + users map[string]*User + upgrader websocket.Upgrader + mu sync.RWMutex +} + +var server *Server + +func init() { + server = &Server{ + users: make(map[string]*User), + upgrader: websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } +} + +func NewUser(id, name, fingerprint string, conn *websocket.Conn) *User { + avatar := "" + if len(name) > 0 { + avatar = string(name[0]) + } + + return &User{ + ID: id, + Name: name, + Avatar: avatar, + Fingerprint: fingerprint, + Conn: conn, + LastSeen: time.Now(), + } +} + +func (u *User) SendMessage(msg Message) error { + u.mu.Lock() + defer u.mu.Unlock() + return u.Conn.WriteJSON(msg) +} + +func (s *Server) BroadcastPublic(msg Message) { + s.mu.RLock() + defer s.mu.RUnlock() + + for fingerprint, user := range s.users { + if fingerprint != msg.From { + go user.SendMessage(msg) + } + } +} + +func (s *Server) SendPrivate(msg Message) error { + s.mu.RLock() + defer s.mu.RUnlock() + + recipient, exists := s.users[msg.To] + if !exists { + return nil + } + + return recipient.SendMessage(msg) +} + +func (s *Server) AddUser(user *User) { + s.mu.Lock() + s.users[user.Fingerprint] = user + s.mu.Unlock() + + log.Printf("User added: %s (fingerprint: %s, total: %d)", user.Name, user.Fingerprint, len(s.users)) + + joinMsg := Message{ + Type: MsgTypeUserJoin, + From: user.Fingerprint, + FromName: user.Name, + FromAvatar: user.Avatar, + Text: user.Name + " joined the chat", + Timestamp: time.Now(), + } + s.BroadcastPublic(joinMsg) + + s.SendUsersList(user) + s.BroadcastUsersList() +} + +func (s *Server) RemoveUser(fingerprint string) { + s.mu.Lock() + user, exists := s.users[fingerprint] + if exists { + delete(s.users, fingerprint) + } + s.mu.Unlock() + + if exists { + log.Printf("User removed: %s (fingerprint: %s, remaining: %d)", user.Name, fingerprint, len(s.users)) + + leaveMsg := Message{ + Type: MsgTypeUserLeave, + From: fingerprint, + FromName: user.Name, + Text: user.Name + " left the chat", + Timestamp: time.Now(), + } + s.BroadcastPublic(leaveMsg) + s.BroadcastUsersList() + } +} + +func (s *Server) SendUsersList(user *User) { + s.mu.RLock() + defer s.mu.RUnlock() + + users := make([]UserInfo, 0, len(s.users)) + for _, u := range s.users { + if u.Fingerprint != user.Fingerprint { + users = append(users, UserInfo{ + ID: u.Fingerprint, + Name: u.Name, + Avatar: u.Avatar, + Status: "š Encrypted", + }) + } + } + + usersData, _ := json.Marshal(map[string]interface{}{ + "type": MsgTypeUsersList, + "users": users, + }) + + user.mu.Lock() + user.Conn.WriteMessage(websocket.TextMessage, usersData) + user.mu.Unlock() +} + +func (s *Server) BroadcastUsersList() { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, user := range s.users { + go s.SendUsersList(user) + } +} + +func HandleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := server.upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + + var handshake struct { + Type string `json:"type"` + Name string `json:"name"` + ID string `json:"id"` + Fingerprint string `json:"fingerprint"` + } + + err = conn.ReadJSON(&handshake) + if err != nil { + log.Printf("Handshake error: %v", err) + conn.Close() + return + } + + if handshake.Type != MsgTypeHandshake { + log.Printf("Invalid handshake type: %s", handshake.Type) + conn.Close() + return + } + + if handshake.Fingerprint == "" || len(handshake.Fingerprint) < 8 { + log.Printf("Invalid fingerprint") + conn.Close() + return + } + + server.mu.RLock() + _, exists := server.users[handshake.Fingerprint] + server.mu.RUnlock() + + if exists { + errorMsg := Message{ + Type: MsgTypeSystem, + Text: "Identity already connected. Only one session per identity allowed.", + } + conn.WriteJSON(errorMsg) + conn.Close() + return + } + + user := NewUser(handshake.ID, handshake.Name, handshake.Fingerprint, conn) + server.AddUser(user) + defer server.RemoveUser(user.Fingerprint) + + log.Printf("User connected: %s (%s)", user.Name, user.Fingerprint) + + welcomeMsg := Message{ + Type: MsgTypeSystem, + Text: "Welcome to NoshiTalk! Your identity is cryptographically protected.", + Timestamp: time.Now(), + } + user.SendMessage(welcomeMsg) + + for { + var msg Message + err := conn.ReadJSON(&msg) + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("WebSocket error: %v", err) + } + break + } + + msg.From = user.Fingerprint + msg.FromName = user.Name + msg.FromAvatar = user.Avatar + msg.Timestamp = time.Now() + + switch msg.Type { + case MsgTypePublic: + server.BroadcastPublic(msg) + log.Printf("Public message from %s: %s", user.Name, msg.Text) + + case MsgTypePrivate: + err := server.SendPrivate(msg) + if err != nil { + log.Printf("Private message error: %v", err) + } else { + log.Printf("Private message from %s to %s", user.Name, msg.To) + } + } + + user.LastSeen = time.Now() + } +} + +func HandleIndex(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("Referrer-Policy", "no-referrer") + + w.Write([]byte(htmlTemplate)) +} + +func generateChallenge() (string, error) { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} + +func (s *Server) CleanupInactiveUsers() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for range ticker.C { + s.mu.Lock() + now := time.Now() + for fingerprint, user := range s.users { + if now.Sub(user.LastSeen) > 5*time.Minute { + log.Printf("Removing inactive user: %s", user.Name) + delete(s.users, fingerprint) + user.Conn.Close() + } + } + s.mu.Unlock() + } +} + +const htmlTemplate = ` + +
+ + +