summaryrefslogtreecommitdiffstats
path: root/cmd/web-client/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/web-client/main.go')
-rw-r--r--cmd/web-client/main.go360
1 files changed, 360 insertions, 0 deletions
diff --git a/cmd/web-client/main.go b/cmd/web-client/main.go
new file mode 100644
index 0000000..d5322a3
--- /dev/null
+++ b/cmd/web-client/main.go
@@ -0,0 +1,360 @@
+package main
+
+import (
+ "crypto/rand"
+ _ "embed"
+ "encoding/hex"
+ "encoding/json"
+ "log"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+)
+
+//go:embed template.html
+var htmlTemplate string
+
+// 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,
+ },
+ }
+}
+
+// NewUser creates a new user.
+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(),
+ }
+}
+
+// SendMessage sends a message to the user.
+func (u *User) SendMessage(msg Message) error {
+ u.mu.Lock()
+ defer u.mu.Unlock()
+ return u.Conn.WriteJSON(msg)
+}
+
+// BroadcastPublic broadcasts a message to all users except the sender.
+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)
+ }
+ }
+}
+
+// SendPrivate sends a private message to a specific user.
+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)
+}
+
+// AddUser adds a user to the server.
+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()
+}
+
+// RemoveUser removes a user from the server.
+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()
+ }
+}
+
+// SendUsersList sends the user list to a specific user.
+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()
+}
+
+// BroadcastUsersList broadcasts the user list to all users.
+func (s *Server) BroadcastUsersList() {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ for _, user := range s.users {
+ go s.SendUsersList(user)
+ }
+}
+
+// HandleWebSocket handles WebSocket connections.
+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"`
+ }
+
+ if err := conn.ReadJSON(&handshake); 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
+ if err := conn.ReadJSON(&msg); 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:
+ if err := server.SendPrivate(msg); 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()
+ }
+}
+
+// HandleIndex serves the main HTML page.
+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))
+}
+
+// generateChallenge generates a random challenge.
+func generateChallenge() (string, error) {
+ b := make([]byte, 32)
+ if _, err := rand.Read(b); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(b), nil
+}
+
+// CleanupInactiveUsers removes inactive users periodically.
+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()
+ }
+}
+
+func main() {
+ go server.CleanupInactiveUsers()
+
+ http.HandleFunc("/", HandleIndex)
+ http.HandleFunc("/ws", HandleWebSocket)
+
+ port := ":8080"
+ log.Printf("===========================================")
+ log.Printf(" NoshiTalk Web Server v2.0-refactored")
+ log.Printf("===========================================")
+ log.Printf(" Port: %s", port)
+ log.Printf(" WebSocket: ws://localhost%s/ws", port)
+ log.Printf(" Security: E2E Encrypted + Ephemeral")
+ log.Printf(" Storage: Zero (RAM only)")
+ log.Printf("===========================================")
+
+ if err := http.ListenAndServe(port, nil); err != nil {
+ log.Fatal("Server error: ", err)
+ }
+}