package srv
import (
db "chat/db"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type Config struct {
Server struct {
IpAddress string `json:"ipAddress"`
Port int `json:"port"`
} `json:"server"`
Paths struct {
DatabasePath string `json:"databasePath"`
IndexJsPath string `json:"indexJsPath"`
IndexCssPath string `json:"indexCssPath"`
IndexHtmlPath string `json:"indexHtmlPath"`
SignupHtmlPath string `json:"signupHtmlPath"`
LoginHtmlPath string `json:"loginHtmlPath"`
} `json:"paths"`
Options struct {
MessageMaxAge int `json:"messageMaxAge"`
NameMaxLength int `json:"nameMaxLength"`
MessagePerPage int `json:"messagePerPage"`
} `json:"options"`
}
func LoadConfig(filepath string) Config {
contents, _ := os.ReadFile(filepath)
var config Config
err := json.Unmarshal(contents, &config)
config.Paths.IndexHtmlPath = pathMaker(config.Paths.IndexHtmlPath)
config.Paths.IndexJsPath = pathMaker(config.Paths.IndexJsPath)
config.Paths.IndexCssPath = pathMaker(config.Paths.IndexCssPath)
config.Paths.SignupHtmlPath = pathMaker(config.Paths.SignupHtmlPath)
config.Paths.LoginHtmlPath = pathMaker(config.Paths.LoginHtmlPath)
config.Paths.DatabasePath = pathMaker(config.Paths.DatabasePath)
if err != nil {
fmt.Println("Error parsing config file: ", err)
os.Exit(1)
}
return config
}
func pathMaker(path string) string {
absPath, _ := filepath.Abs(path)
absPath = filepath.Clean(absPath)
fmt.Println(absPath)
return absPath
}
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 getMessageTemplate(body string) string {
template := `
{{body}}
`
return strings.Replace(template, "{{body}}", body, 1)
}
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
}
func readFile(filepath string) []byte {
contents, _ := os.ReadFile(filepath)
return contents
}
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
}
type Server struct {
LoggedIn map[string]string // Map Username -> IP
Connected map[string]time.Time // Map IP -> Last activity time
Database *db.Database
Config Config
mu sync.Mutex // For thread safety
}
func NewServer(config Config) *Server {
return &Server{
LoggedIn: make(map[string]string),
Connected: make(map[string]time.Time),
Database: db.OpenDatabase(config.Paths.DatabasePath),
Config: config,
mu: sync.Mutex{},
}
}
func (s *Server) AddMessage(userip string, contents string) {
s.Database.MessageAdd(userip, contents)
}
func (s *Server) LogUserIn(ip, username string) {
s.mu.Lock()
defer s.mu.Unlock()
s.LoggedIn[ip] = username
}
func (s *Server) LogUserOut(username string) {
s.mu.Lock()
defer s.mu.Unlock()
for ip, u := range s.LoggedIn {
if u == username {
delete(s.LoggedIn, ip)
}
}
delete(s.LoggedIn, username)
}
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)
s.LogUserOut(s.LoggedIn[ip])
}
}
}
func (s *Server) Run() {
s.Database.DbCreateTableMessages()
s.Database.DbCreateTableUsers()
s.Database.DeleteOldMessages(s.Config.Options.MessageMaxAge)
handler := http.NewServeMux()
handler.HandleFunc("/", s.handleRoot)
handler.HandleFunc("/root.js", s.handleJs)
handler.HandleFunc("/root.css", s.handleCss)
handler.HandleFunc("/ping", s.handlePing)
handler.HandleFunc("/timezone", s.handleTimezone)
handler.HandleFunc("/username/status", s.handleUsernameStatus)
handler.HandleFunc("/messages/length", s.handleMessagesLength)
handler.HandleFunc("/users", s.handleUsers)
handler.HandleFunc("/username", s.handleUsername)
handler.HandleFunc("/messages", s.handleMessages)
handler.HandleFunc("/signup", s.handleSignup)
handler.HandleFunc("/login", s.handleLogin)
fmt.Printf("Server starting on %s:%d\n", s.Config.Server.IpAddress, s.Config.Server.Port)
defer s.Stop()
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", s.Config.Server.IpAddress, s.Config.Server.Port), GzipMiddleware(handler)); err != nil {
fmt.Printf("Server error: %v\n", err)
}
}
func (s *Server) Stop() {
s.Database.Close()
}