internal/honeypot/http/handler.go

package http

import (
	"honeypot/internal/logger"
	"honeypot/internal/types"
	"io/fs"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// wpFormsHandler handles POST requests to /wp-admin/admin-post.php.
func (h *httpHoneypot) wpFormsHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fields := h.buildRequestFields(r)
		h.addHeadersToFields(r, fields)

		// read post time
		postTime := r.FormValue("form_nonce")
		if postTime == "" || postTime == "0" {
			fields["time_diff"] = "zero"
		}

		// calculate time difference between post time and current time
		postTimeInt, err := strconv.ParseInt(postTime, 10, 64)
		if err != nil {
			fields["time_diff"] = "invalid"
		}
		timeDiff := time.Now().Unix() - postTimeInt
		fields["time_diff"] = timeDiff

		email := r.FormValue("email")
		fields["email"] = email
		name := r.FormValue("name")
		fields["name"] = name
		message := r.FormValue("message")
		fields["message"] = message

		remoteHost, remotePort := h.getRemoteAddr(r)
		var dstPort uint16
		if port, ok := r.Context().Value(dstPortKey).(uint16); ok {
			dstPort = port
		}

		event := types.LogEvent{
			Type:       HoneypotType,
			Event:      "contact_form",
			RemoteAddr: remoteHost,
			RemotePort: remotePort,
			DstPort:    dstPort,
			Fields:     fields,
		}

		logger.LogEvent(h.logger, event)
		h.recordHTTPMetrics(event)

		w.Header().Add("Location", "/?success=true#kontakt")
		w.WriteHeader(http.StatusMovedPermanently)
		w.Write([]byte("Form submission received"))
	})
}

// wpAdminHandler handles WordPress admin routes, redirecting to login page or serving static files.
func (h *httpHoneypot) wpAdminHandler(staticRoot fs.FS) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Redirect /wp-admin/ and /wp-admin to wp-login.php
		if r.URL.Path == "/wp-admin/" || r.URL.Path == "/wp-admin" {
			http.Redirect(w, r, "/wp-login.php", http.StatusFound)
			return
		}
		// Serve static files under /wp-admin/
		h.serveFile(w, r, r.URL.Path)
	})
}

// staticFileHandler serves files from the static directory.
func (h *httpHoneypot) staticFileHandler(staticRoot fs.FS) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		filename := h.resolveStaticPath(r.URL.Path)
		h.serveFile(w, r, filename)
	})
}

func (h *httpHoneypot) fakeAdminOK(w http.ResponseWriter, r *http.Request) {
	h.setResponseHeaders(w)
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`
<!DOCTYPE html>
<html>
	<head><title>Admin Panel</title></head>
	<body>
		<h1>Admin Panel</h1>
		<p>Access denied.</p>
	</body>
</html>
`))
}

func (h *httpHoneypot) fakeBearerProtectedHandler(realm string, resource string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		auth := r.Header.Get("Authorization")
		var token string

		if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
			token = strings.TrimSpace(auth[len("Bearer "):])
		}

		fields := h.buildRequestFields(r)
		h.addHeadersToFields(r, fields)

		event := types.LogEvent{}

		if token != "" {
			fields["auth_type"] = "bearer"
			fields["token"] = token
			fields["realm"] = realm
			if resource != "" {
				fields["resource"] = resource
			}

			event.Event = types.EventAuthAttempt
		} else {
			event.Event = types.EventRequest
		}

		remoteHost, remotePort := h.getRemoteAddr(r)
		var dstPort uint16
		if port, ok := r.Context().Value(dstPortKey).(uint16); ok {
			dstPort = port
		}

		event.Type = HoneypotType
		event.RemoteAddr = remoteHost
		event.RemotePort = remotePort
		event.DstPort = dstPort
		event.Fields = fields

		logger.LogEvent(h.logger, event)
		h.recordHTTPMetrics(event)

		if token != "" && looksLikeBearerToken(token) {
			h.writeNginxError(w, http.StatusForbidden)
			return
		}

		w.Header().Set(
			"WWW-Authenticate",
			`Bearer realm="`+realm+`", error="invalid_token"`,
		)
		h.writeNginxError(w, http.StatusUnauthorized)
	})
}

func (h *httpHoneypot) serveWordPressPHP(w http.ResponseWriter, r *http.Request) {
	h.setResponseHeaders(w)
	w.Header().Set("Content-Type", "text/html; charset=UTF-8")
	w.WriteHeader(http.StatusOK)
	// Intentionally empty body
}