package handlers import ( "encoding/json" "net" "net/http" "strings" "git.eeqj.de/sneak/neoirc/internal/db" ) const minPasswordLength = 8 // clientIP extracts the client IP address from the request. // It checks X-Forwarded-For and X-Real-IP headers before // falling back to RemoteAddr. func clientIP(request *http.Request) string { if forwarded := request.Header.Get("X-Forwarded-For"); forwarded != "" { // X-Forwarded-For may contain a comma-separated list; // the first entry is the original client. parts := strings.SplitN(forwarded, ",", 2) //nolint:mnd // split into two parts ip := strings.TrimSpace(parts[0]) if ip != "" { return ip } } if realIP := request.Header.Get("X-Real-IP"); realIP != "" { return strings.TrimSpace(realIP) } host, _, err := net.SplitHostPort(request.RemoteAddr) if err != nil { return request.RemoteAddr } return host } // HandleRegister creates a new user with a password. func (hdlr *Handlers) HandleRegister() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { request.Body = http.MaxBytesReader( writer, request.Body, hdlr.maxBodySize(), ) hdlr.handleRegister(writer, request) } } func (hdlr *Handlers) handleRegister( writer http.ResponseWriter, request *http.Request, ) { type registerRequest struct { Nick string `json:"nick"` Password string `json:"password"` } var payload registerRequest err := json.NewDecoder(request.Body).Decode(&payload) if err != nil { hdlr.respondError( writer, request, "invalid request body", http.StatusBadRequest, ) return } payload.Nick = strings.TrimSpace(payload.Nick) if !validNickRe.MatchString(payload.Nick) { hdlr.respondError( writer, request, "invalid nick format", http.StatusBadRequest, ) return } if len(payload.Password) < minPasswordLength { hdlr.respondError( writer, request, "password must be at least 8 characters", http.StatusBadRequest, ) return } sessionID, clientID, token, err := hdlr.params.Database.RegisterUser( request.Context(), payload.Nick, payload.Password, ) if err != nil { hdlr.handleRegisterError( writer, request, err, ) return } hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick) hdlr.respondJSON(writer, request, map[string]any{ "id": sessionID, "nick": payload.Nick, "token": token, }, http.StatusCreated) } func (hdlr *Handlers) handleRegisterError( writer http.ResponseWriter, request *http.Request, err error, ) { if db.IsUniqueConstraintError(err) { hdlr.respondError( writer, request, "nick already taken", http.StatusConflict, ) return } hdlr.log.Error( "register user failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) } // HandleLogin authenticates a user with nick and password. func (hdlr *Handlers) HandleLogin() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { request.Body = http.MaxBytesReader( writer, request.Body, hdlr.maxBodySize(), ) hdlr.handleLogin(writer, request) } } func (hdlr *Handlers) handleLogin( writer http.ResponseWriter, request *http.Request, ) { ip := clientIP(request) if !hdlr.loginLimiter.Allow(ip) { writer.Header().Set( "Retry-After", "1", ) hdlr.respondError( writer, request, "too many login attempts, try again later", http.StatusTooManyRequests, ) return } type loginRequest struct { Nick string `json:"nick"` Password string `json:"password"` } var payload loginRequest err := json.NewDecoder(request.Body).Decode(&payload) if err != nil { hdlr.respondError( writer, request, "invalid request body", http.StatusBadRequest, ) return } payload.Nick = strings.TrimSpace(payload.Nick) if payload.Nick == "" || payload.Password == "" { hdlr.respondError( writer, request, "nick and password required", http.StatusBadRequest, ) return } sessionID, clientID, token, err := hdlr.params.Database.LoginUser( request.Context(), payload.Nick, payload.Password, ) if err != nil { hdlr.respondError( writer, request, "invalid credentials", http.StatusUnauthorized, ) return } hdlr.deliverMOTD( request, clientID, sessionID, payload.Nick, ) // Initialize channel state so the new client knows // which channels the session already belongs to. hdlr.initChannelState( request, clientID, sessionID, payload.Nick, ) hdlr.respondJSON(writer, request, map[string]any{ "id": sessionID, "nick": payload.Nick, "token": token, }, http.StatusOK) }