367 lines
9.0 KiB
Go
367 lines
9.0 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"compress/gzip"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type gzipResponseWriter struct {
|
||
|
http.ResponseWriter
|
||
|
io.Writer
|
||
|
}
|
||
|
|
||
|
func (g *gzipResponseWriter) Write(data []byte) (int, error) {
|
||
|
return g.Writer.Write(data)
|
||
|
}
|
||
|
|
||
|
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 gz.Close()
|
||
|
|
||
|
w.Header().Set("Content-Encoding", "gzip")
|
||
|
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func getClientIP(r *http.Request) string {
|
||
|
if fwdIP := r.Header.Get("X-Forwarded-For"); fwdIP != "" {
|
||
|
return strings.Split(fwdIP, ",")[0]
|
||
|
}
|
||
|
|
||
|
clientIP := r.RemoteAddr
|
||
|
if host, _, err := net.SplitHostPort(clientIP); err == nil {
|
||
|
return host
|
||
|
}
|
||
|
return clientIP
|
||
|
}
|
||
|
|
||
|
type Message struct {
|
||
|
Content string
|
||
|
SenderIp string
|
||
|
SenderUsername string
|
||
|
Timestamp string
|
||
|
}
|
||
|
|
||
|
func NewMessage(username, ip, content string) *Message {
|
||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||
|
return &Message{
|
||
|
Content: content,
|
||
|
SenderIp: ip,
|
||
|
SenderUsername: username,
|
||
|
Timestamp: timestamp,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO: Use it
|
||
|
func writeMessageToFile(msg Message) {
|
||
|
contents := fmt.Sprintf("%s,%s,%s,%s\n", msg.SenderUsername, msg.SenderIp, msg.Timestamp, msg.Content)
|
||
|
os.WriteFile("messages.txt", []byte(contents), os.ModeAppend)
|
||
|
}
|
||
|
|
||
|
// TODO: Use it
|
||
|
func writeMessagesToFile(messages []Message) {
|
||
|
var contents string
|
||
|
for _, msg := range messages {
|
||
|
contents += fmt.Sprintf("%s,%s,%s,%s\n", msg.SenderUsername, msg.SenderIp, msg.Timestamp, msg.Content)
|
||
|
}
|
||
|
os.WriteFile("messages.txt", []byte(contents), os.ModeAppend)
|
||
|
}
|
||
|
|
||
|
// TODO: Use it
|
||
|
func readMessagesFromFile(filepath string) []Message {
|
||
|
contents, _ := os.ReadFile(filepath)
|
||
|
lines := strings.Split(string(contents), "\n")
|
||
|
var messages []Message
|
||
|
for _, line := range lines {
|
||
|
if line == "" {
|
||
|
continue
|
||
|
}
|
||
|
parts := strings.Split(line, ",")
|
||
|
username := parts[0]
|
||
|
timestamp := parts[1]
|
||
|
content := strings.Join(parts[2:], " ")
|
||
|
messages = append(messages, Message{username, "", content, timestamp})
|
||
|
}
|
||
|
return messages
|
||
|
}
|
||
|
|
||
|
type Server struct {
|
||
|
Ip string
|
||
|
Port int
|
||
|
Messages []Message
|
||
|
// Add mappings for IPs and usernames
|
||
|
Connected map[string]time.Time // Map IP -> Last activity time
|
||
|
Usernames map[string]string // Map IP -> Username
|
||
|
mu sync.Mutex // For thread safety
|
||
|
}
|
||
|
|
||
|
func NewServer(ip string, port int) *Server {
|
||
|
return &Server{
|
||
|
Ip: ip,
|
||
|
Port: port,
|
||
|
Messages: make([]Message, 0),
|
||
|
Connected: make(map[string]time.Time),
|
||
|
Usernames: make(map[string]string),
|
||
|
mu: sync.Mutex{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Server) AddMessage(username string, userip string, contents string) {
|
||
|
message := NewMessage(username, userip, contents)
|
||
|
s.Messages = append(s.Messages, *message)
|
||
|
}
|
||
|
|
||
|
func (s *Server) updateActivity(ip string) {
|
||
|
s.mu.Lock()
|
||
|
defer s.mu.Unlock()
|
||
|
s.Connected[ip] = time.Now()
|
||
|
}
|
||
|
|
||
|
func (s *Server) cleanupActivity() {
|
||
|
s.mu.Lock()
|
||
|
defer s.mu.Unlock()
|
||
|
for ip, lastActivity := range s.Connected {
|
||
|
if time.Since(lastActivity) > 10*time.Second {
|
||
|
delete(s.Connected, ip)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Server) handlePing(w http.ResponseWriter, r *http.Request) {
|
||
|
clientIP := getClientIP(r)
|
||
|
s.updateActivity(clientIP)
|
||
|
s.cleanupActivity()
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
}
|
||
|
|
||
|
func validUsername(username string) bool {
|
||
|
for _, c := range username {
|
||
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleUsername(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method != http.MethodPut {
|
||
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
||
|
clientIP := getClientIP(r)
|
||
|
|
||
|
var req struct {
|
||
|
Username string `json:"username"`
|
||
|
}
|
||
|
|
||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
s.mu.Lock()
|
||
|
|
||
|
if len(req.Username) > 64 {
|
||
|
http.Error(w, fmt.Sprintf(`{"error": "Username too long (must be less than 64 characters)"}`), http.StatusRequestEntityTooLarge)
|
||
|
s.mu.Unlock()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if !validUsername(req.Username) {
|
||
|
http.Error(w, fmt.Sprintf(`{"error": "Username must only contain alphanumeric characters and/or underscores"}`), http.StatusBadRequest)
|
||
|
s.mu.Unlock()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
for _, username := range s.Usernames {
|
||
|
if username == req.Username {
|
||
|
s.mu.Unlock()
|
||
|
http.Error(w, fmt.Sprintf(`{"error": "Username already exists"}`), http.StatusConflict)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if _, ok := s.Usernames[clientIP]; ok {
|
||
|
fmt.Println("Name change detected, fixing messages to reflect the change")
|
||
|
for i, msg := range s.Messages {
|
||
|
if msg.SenderIp == clientIP {
|
||
|
s.Messages[i].SenderUsername = req.Username
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
s.Usernames[clientIP] = req.Username
|
||
|
|
||
|
s.mu.Unlock()
|
||
|
|
||
|
json.NewEncoder(w).Encode(map[string]string{"status": "Username registered"})
|
||
|
fmt.Println(clientIP, "->", req.Username)
|
||
|
}
|
||
|
|
||
|
func getMessageTemplate(file string, body string) string {
|
||
|
contents, _ := os.ReadFile(file)
|
||
|
return strings.Replace(string(contents), "{{body}}", body, 1)
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
|
||
|
switch r.Method {
|
||
|
case http.MethodGet:
|
||
|
w.Header().Set("Content-Type", "text/html")
|
||
|
|
||
|
var body string
|
||
|
for _, msg := range s.Messages {
|
||
|
body += fmt.Sprintf(`<p><span class="username">%s </span><span class="timestamp">%s</span><br><span class="message">%s</span></p>`,
|
||
|
msg.SenderUsername, msg.Timestamp, msg.Content)
|
||
|
}
|
||
|
|
||
|
w.Write([]byte(getMessageTemplate("messages.html", body)))
|
||
|
|
||
|
case http.MethodPut:
|
||
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
||
|
// Get client's IP
|
||
|
clientIP := getClientIP(r)
|
||
|
|
||
|
// Check if IP is registered
|
||
|
s.mu.Lock()
|
||
|
username, exists := s.Usernames[clientIP]
|
||
|
s.mu.Unlock()
|
||
|
|
||
|
if !exists {
|
||
|
errorFmt := fmt.Sprintf(`{"error": "IP %s not registered with username"}`, clientIP)
|
||
|
http.Error(w, errorFmt, http.StatusUnauthorized)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var msg struct {
|
||
|
Message string `json:"message"`
|
||
|
}
|
||
|
|
||
|
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
|
||
|
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Add message using the stored username and IP
|
||
|
s.AddMessage(username, clientIP, msg.Message)
|
||
|
|
||
|
json.NewEncoder(w).Encode(map[string]string{
|
||
|
"status": "Message received",
|
||
|
"from": username,
|
||
|
})
|
||
|
default:
|
||
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleUsernameStatus(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method != http.MethodGet {
|
||
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
w.Header().Set("Content-Type", "application/json")
|
||
|
clientIP := getClientIP(r)
|
||
|
|
||
|
s.mu.Lock()
|
||
|
username, exists := s.Usernames[clientIP]
|
||
|
s.mu.Unlock()
|
||
|
|
||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
|
"hasUsername": exists,
|
||
|
"username": username,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method != http.MethodGet {
|
||
|
http.Error(w, `{"error": "Method not allowed"}`, http.StatusMethodNotAllowed)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
||
|
clientIP := getClientIP(r)
|
||
|
s.updateActivity(clientIP)
|
||
|
s.cleanupActivity()
|
||
|
s.mu.Lock()
|
||
|
var users []string
|
||
|
for ip := range s.Connected {
|
||
|
users = append(users, s.Usernames[ip])
|
||
|
}
|
||
|
s.mu.Unlock()
|
||
|
|
||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||
|
"users": users,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.URL.Path != "/" {
|
||
|
http.NotFound(w, r)
|
||
|
return
|
||
|
}
|
||
|
w.Header().Set("Content-Type", "text/html")
|
||
|
w.Write(readFile("root.html"))
|
||
|
}
|
||
|
|
||
|
func readFile(filepath string) []byte {
|
||
|
contents, _ := os.ReadFile(filepath)
|
||
|
return contents
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleJs(w http.ResponseWriter, r *http.Request) {
|
||
|
_ = r
|
||
|
w.Header().Set("Content-Type", "application/javascript")
|
||
|
w.Write(readFile("root.js"))
|
||
|
}
|
||
|
|
||
|
func (s *Server) handleCss(w http.ResponseWriter, r *http.Request) {
|
||
|
_ = r
|
||
|
w.Header().Set("Content-Type", "text/css")
|
||
|
w.Write(readFile("root.css"))
|
||
|
}
|
||
|
|
||
|
func (s *Server) Run() {
|
||
|
handler := http.NewServeMux()
|
||
|
handler.HandleFunc("/ping", s.handlePing)
|
||
|
handler.HandleFunc("/username", s.handleUsername)
|
||
|
handler.HandleFunc("/messages", s.handleMessages)
|
||
|
handler.HandleFunc("/username/status", s.handleUsernameStatus)
|
||
|
handler.HandleFunc("/users", s.handleUsers)
|
||
|
handler.HandleFunc("/", s.handleRoot)
|
||
|
handler.HandleFunc("/root.js", s.handleJs)
|
||
|
handler.HandleFunc("/root.css", s.handleCss)
|
||
|
fmt.Printf("Server starting on %s:%d\n", s.Ip, s.Port)
|
||
|
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Ip, s.Port), GzipMiddleware(handler)); err != nil {
|
||
|
fmt.Printf("Server error: %v\n", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
ip := os.Args[1]
|
||
|
port, _ := strconv.Atoi(os.Args[2])
|
||
|
// ip := "localhost"
|
||
|
// port := 8080
|
||
|
server := NewServer(ip, port)
|
||
|
server.Run()
|
||
|
}
|