internal/honeypot/http/http.go

package http

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"honeypot/internal/database"
	"honeypot/internal/honeypot"
	"honeypot/internal/logger"
	"honeypot/internal/metrics"
	tlscert "honeypot/internal/tls"
	"honeypot/internal/types"
	"honeypot/internal/utils"
	"io"
	"io/fs"
	"log/slog"
	"mime/multipart"
	"net/http"
	"net/url"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/prometheus/client_golang/prometheus"
)

const (
	HoneypotType       = types.HoneypotTypeHTTP
	HoneypotLabel      = "HTTP"
	DefaultMaxBodySize = 500 * 1024 // 500 KB
	MaxBodyLogLength   = 10000
	MaxFilePreviewSize = 1024
	CacheMaxAge        = "max-age=2592000" // 30 days
	NginxServerHeader  = "nginx"
	ShutdownTimeout    = 3 * time.Second
)

// contextKey is used for request context values.
type contextKey string

// dstPortKey is the context key for the destination port.
const dstPortKey contextKey = "dstPort"

var (
	// UsernameFieldNames contains common field names that might contain usernames
	UsernameFieldNames = []string{
		"log", "user", "username", "login", "email", "account",
		"accountname", "userid", "user_id", "usr", "name",
	}

	// PasswordFieldNames contains common field names that might contain passwords
	PasswordFieldNames = []string{
		"pwd", "psd", "password", "passwd", "pass", "secret", "pwd1", "pwd2",
	}

	// StaticExtensions contains file extensions that should be skipped in logging
	StaticExtensions = map[string]bool{
		".js": true, ".css": true, ".ico": true, ".png": true,
		".jpg": true, ".jpeg": true, ".gif": true, ".svg": true,
		".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
	}

	// MethodsWithBody contains HTTP methods that typically have request bodies
	MethodsWithBody = map[string]bool{
		http.MethodPost:  true,
		http.MethodPut:   true,
		http.MethodPatch: true,
		"PROPFIND":       true,
		"PROPPATCH":      true,
	}
)

// Config holds the configuration for the HTTP honeypot.
type Config struct {
	ListenAddr     string
	HTTPPorts      []uint16
	HTTPSPorts     []uint16
	MaxBodySize    int64
	CertConfig     tlscert.CertConfig
	TrustedProxies []string
}

// httpHoneypot implements the honeypot.Honeypot interface.
type httpHoneypot struct {
	config      Config
	logger      *slog.Logger
	maxBodySize int64
	httpMethods *prometheus.CounterVec
}

// New creates a new HTTP honeypot instance.
func New(cfg Config) honeypot.Honeypot {
	maxBodySize := cfg.MaxBodySize
	if maxBodySize == 0 {
		maxBodySize = DefaultMaxBodySize
	}

	h := &httpHoneypot{
		config:      cfg,
		maxBodySize: maxBodySize,
	}

	// Register user_agent and uri fields for top-N tracking
	// This is done during creation so fields are registered before log restoration
	logger.RegisterTopNField("http", "user_agent")
	logger.RegisterTopNField("http", "uri")

	// Initialize and register HTTP-specific metrics
	h.httpMethods = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "honeypot_http_methods_total",
			Help: "Total number of HTTP requests by method",
		},
		[]string{"method"},
	)

	// Register the metrics
	if err := logger.RegisterCollector(h.httpMethods); err != nil {
		return nil
	}

	return h
}

// Name returns the name of this honeypot.
func (h *httpHoneypot) Name() types.HoneypotType {
	return HoneypotType
}

// Label returns the label of this honeypot.
func (h *httpHoneypot) Label() string {
	return HoneypotLabel
}

// Start starts the HTTP honeypot server.
func (h *httpHoneypot) Start(ctx context.Context, l *slog.Logger) error {
	h.logger = l

	staticRoot, err := h.setupStaticFS()
	if err != nil {
		return err
	}

	mux := h.setupRoutes(staticRoot)
	var wg sync.WaitGroup

	// Generate self-signed certificate once for all HTTPS servers if needed
	// This is used either as the main cert (no domain) or as fallback (with domain/ACME)
	var selfSignedCert *tls.Certificate
	if len(h.config.HTTPSPorts) > 0 {
		var err error
		selfSignedCert, err = h.generateSelfSignedCert(ctx)
		if err != nil {
			return err
		}
	}

	// Start HTTP servers on all configured ports
	for _, port := range h.config.HTTPPorts {
		if port > 0 {
			server := h.createServer(mux, port, nil)
			h.logServerStart(ctx, port, "http")
			h.startHTTPServer(ctx, server, &wg)
			h.setupGracefulShutdown(ctx, server)
		}
	}

	// Start HTTPS servers on all configured ports
	for _, port := range h.config.HTTPSPorts {
		if port > 0 {
			h.startHTTPSServerIfConfigured(ctx, &wg, mux, port, selfSignedCert)
		}
	}

	wg.Wait()
	logger.LogInfo(h.logger, HoneypotType, "honeypot shutdown complete", nil)
	return nil
}

// setupStaticFS prepares the static filesystem.
func (h *httpHoneypot) setupStaticFS() (fs.FS, error) {
	staticRoot, err := fs.Sub(staticFS, "static")
	if err != nil {
		logger.LogError(h.logger, HoneypotType, "static_fs_failed", err, nil)
		return nil, err
	}
	return staticRoot, nil
}

// createServer creates and configures an HTTP or HTTPS server.
func (h *httpHoneypot) createServer(handler http.Handler, port uint16, tlsConfig *tls.Config) *http.Server {
	// Wrap handler with port middleware so requests know which port they came in on
	wrappedHandler := h.portMiddleware(handler, port)
	server := &http.Server{
		Addr:              utils.BuildAddress(h.config.ListenAddr, port),
		Handler:           wrappedHandler,
		ReadHeaderTimeout: 3 * time.Second,
	}
	if tlsConfig != nil {
		server.TLSConfig = tlsConfig
	}
	return server
}

// logServerStart logs server startup information for HTTP or HTTPS servers.
func (h *httpHoneypot) logServerStart(ctx context.Context, port uint16, serverType string) {
	logFields := map[string]interface{}{
		"addr": utils.BuildAddress(h.config.ListenAddr, port),
		"port": port,
	}

	switch serverType {
	case "http":
		logFields["max_body_size"] = h.maxBodySize
	case "https":
		if len(h.config.HTTPPorts) > 0 {
			logFields["http_ports"] = h.config.HTTPPorts
		}
	}

	message := serverType + " server listening"
	if serverType == "http" {
		message = "honeypot listening"
	}
	logger.LogInfo(h.logger, HoneypotType, message, []any{
		"port", port,
	})
}

// startHTTPServer starts the HTTP server in a goroutine.
func (h *httpHoneypot) startHTTPServer(ctx context.Context, server *http.Server, wg *sync.WaitGroup) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			logger.LogError(h.logger, HoneypotType, "http_server_failed", err, nil)
		}
	}()
}

// startHTTPSServerIfConfigured starts the HTTPS server on the specified port.
func (h *httpHoneypot) startHTTPSServerIfConfigured(ctx context.Context, wg *sync.WaitGroup, mux *http.ServeMux, port uint16, selfSignedCert *tls.Certificate) {
	wg.Add(1)
	go func() {
		defer wg.Done()
		if err := h.startHTTPSServer(ctx, mux, port, selfSignedCert); err != nil && err != http.ErrServerClosed {
			logger.LogError(h.logger, HoneypotType, "https_server_failed", err, nil)
		}
	}()
}

// setupGracefulShutdown handles graceful shutdown of an HTTP/HTTPS server.
func (h *httpHoneypot) setupGracefulShutdown(ctx context.Context, server *http.Server) {
	go func() {
		<-ctx.Done()
		shutdownCtx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout)
		defer cancel()
		if err := server.Shutdown(shutdownCtx); err != nil {
			logger.LogError(h.logger, HoneypotType, "server_stop", err, nil)
		}
	}()
}

// resolveStaticPath maps request paths to static file paths.
func (h *httpHoneypot) resolveStaticPath(path string) string {
	switch path {
	case "/", "", "/index.php":
		return "/index.html"
	case "/wp-login.php", "/admin/":
		return "/wp-login.html"
	default:
		return path
	}
}

// serveFile serves a static file with appropriate headers and method validation.
func (h *httpHoneypot) serveFile(w http.ResponseWriter, r *http.Request, filename string) {
	// Method validation first
	if !h.isValidHTTPMethod(r.Method) {
		h.writeNginxError(w, http.StatusMethodNotAllowed)
		return
	}

	// WordPress PHP handler
	if strings.HasSuffix(filename, ".php") &&
		(strings.HasPrefix(filename, "/wp-") || strings.HasPrefix(filename, "/wp-admin/")) {

		switch filename {
		case "/wp-login.php", "/xmlrpc.php", "/index.php":
			// handled normally (static or special handler)
		default:
			h.serveWordPressPHP(w, r)
			return
		}
	}

	// Resolve path inside static FS
	path := "static" + filename

	info, err := fs.Stat(staticFS, path)
	if err != nil {
		// File does not exist
		h.writeNginxError(w, http.StatusNotFound)
		return
	}

	// Directories without index.html → 403
	if info.IsDir() {
		indexPath := path + "/index.html"
		if _, err := fs.Stat(staticFS, indexPath); err != nil {
			h.writeNginxError(w, http.StatusForbidden)
			return
		}
	}

	// Match nginx headers
	h.setResponseHeaders(w)

	// HEAD should not write body
	if r.Method == http.MethodHead {
		w.WriteHeader(http.StatusOK)
		return
	}

	http.ServeFileFS(w, r, staticFS, path)
}

// isValidHTTPMethod checks if the HTTP method is valid.
func (h *httpHoneypot) isValidHTTPMethod(method string) bool {
	validMethods := map[string]bool{
		http.MethodGet:     true,
		http.MethodHead:    true,
		http.MethodPost:    true,
		http.MethodPut:     true,
		http.MethodPatch:   true,
		http.MethodDelete:  true,
		http.MethodOptions: true,
		http.MethodTrace:   true,
		http.MethodConnect: true,
	}
	return validMethods[method]
}

// setResponseHeaders sets standard response headers.
func (h *httpHoneypot) setResponseHeaders(w http.ResponseWriter) {
	w.Header().Add("cache-control", CacheMaxAge)
	w.Header().Add("server", NginxServerHeader)
}

// logRequest logs an HTTP request with all relevant details.
func (h *httpHoneypot) logRequest(r *http.Request) {
	if h.shouldSkipLogging(r.URL.Path) {
		return
	}

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

	// Check if both username and password are present
	username, hasUsername := fields["username"].(string)
	password, hasPassword := fields["password"].(string)
	hasUsername = hasUsername && username != ""
	hasPassword = hasPassword && password != ""

	// If both username and password are present, log as auth_attempt instead of request
	var event types.LogEvent
	if hasUsername && hasPassword {
		event = types.LogEvent{
			Type:       HoneypotType,
			Event:      types.EventAuthAttempt,
			RemoteAddr: remoteHost,
			RemotePort: remotePort,
			DstPort:    dstPort,
			Fields:     fields, // Include all fields including username and password
		}
	} else {
		event = types.LogEvent{
			Type:       HoneypotType,
			Event:      types.EventRequest,
			RemoteAddr: remoteHost,
			RemotePort: remotePort,
			DstPort:    dstPort,
			Fields:     fields,
		}
	}

	logger.LogEvent(h.logger, event)

	// Record HTTP-specific metrics
	h.recordHTTPMetrics(event)
}

// getRemoteAddr resolves the actual remote address, considering proxy headers if the request is from a trusted source.
func (h *httpHoneypot) getRemoteAddr(r *http.Request) (string, uint16) {
	remoteHost, remotePort := utils.SplitAddr(r.RemoteAddr, h.logger)

	// Check if the remote host is trusted
	isTrusted := remoteHost == "127.0.0.1" || remoteHost == "::1"
	if !isTrusted {
		for _, proxy := range h.config.TrustedProxies {
			if remoteHost == proxy {
				isTrusted = true
				break
			}
		}
	}

	// If request is from a trusted proxy, check for proxy headers
	if isTrusted {
		// Try X-Real-Ip first
		if realIP := r.Header.Get("X-Real-Ip"); realIP != "" {
			return realIP, remotePort
		}

		// Fallback to X-Forwarded-For
		if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
			// X-Forwarded-For can contain multiple IPs: client, proxy1, proxy2...
			// We take the first one which is usually the original client.
			ips := strings.Split(xff, ",")
			if len(ips) > 0 {
				return strings.TrimSpace(ips[0]), remotePort
			}
		}
	}

	return remoteHost, remotePort
}

// shouldSkipLogging returns true if the path should not be logged (static assets).
func (h *httpHoneypot) shouldSkipLogging(path string) bool {
	ext := filepath.Ext(path)
	return StaticExtensions[ext]
}

// buildRequestFields creates the base fields for request logging.
func (h *httpHoneypot) buildRequestFields(r *http.Request) map[string]interface{} {
	return map[string]interface{}{
		"method": r.Method,
		"host":   r.Host,
		"uri":    r.URL.Path,
		"query":  r.URL.RawQuery,
	}
}

// addHeadersToFields adds HTTP headers to the log fields.
func (h *httpHoneypot) addHeadersToFields(r *http.Request, fields map[string]interface{}) {
	if len(r.Header) == 0 {
		return
	}

	headers := make(map[string]string)
	for k, v := range r.Header {
		if len(v) > 0 {
			headers[k] = v[0]
		}
	}
	fields["headers"] = headers
}

// logRequestBodyIfNeeded logs the request body if the method typically has a body.
func (h *httpHoneypot) logRequestBodyIfNeeded(r *http.Request, fields map[string]interface{}) {
	if r.Body == nil || !MethodsWithBody[r.Method] {
		return
	}

	bodyBytes, err := h.readAndRestoreBody(r)
	if err != nil {
		return
	}

	if len(bodyBytes) == 0 {
		return
	}

	contentType := r.Header.Get("Content-Type")
	h.parseBodyByContentType(bodyBytes, contentType, fields)
}

// readAndRestoreBody reads the request body and restores it for further processing.
func (h *httpHoneypot) readAndRestoreBody(r *http.Request) ([]byte, error) {
	bodyBytes, err := io.ReadAll(r.Body)
	// Always restore the body, even if there was an error (may have partial data)
	r.Body = io.NopCloser(bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, err
	}
	return bodyBytes, nil
}

// parseBodyByContentType parses the body based on its content type.
func (h *httpHoneypot) parseBodyByContentType(bodyBytes []byte, contentType string, fields map[string]interface{}) {
	fields["content_length"] = len(bodyBytes)
	fields["content_type"] = contentType

	switch {
	case strings.HasPrefix(contentType, "multipart/form-data"):
		h.parseMultipartForm(bodyBytes, contentType, fields)
	case contentType == "application/x-www-form-urlencoded":
		h.parseFormURLEncoded(bodyBytes, fields)
	case strings.HasPrefix(contentType, "application/json"):
		h.parseJSON(bodyBytes, fields)
	default:
		h.logBodyAsString(bodyBytes, fields)
	}
}

// logBodyAsString logs the body as a string, truncating if necessary.
func (h *httpHoneypot) logBodyAsString(bodyBytes []byte, fields map[string]interface{}) {
	bodyStr := string(bodyBytes)
	if len(bodyStr) > MaxBodyLogLength {
		bodyStr = bodyStr[:MaxBodyLogLength] + "... (truncated)"
	}
	fields["body"] = bodyStr
	fields["body_size"] = len(bodyBytes)
}

// parseMultipartForm parses a multipart/form-data request body.
func (h *httpHoneypot) parseMultipartForm(bodyBytes []byte, contentType string, fields map[string]interface{}) {
	boundary := h.extractMultipartBoundary(contentType)
	if boundary == "" {
		// no boundary found, skip parsing
		return
	}

	reader := multipart.NewReader(bytes.NewReader(bodyBytes), boundary)
	formData, files := h.parseMultipartParts(reader, fields)

	if len(formData) > 0 {
		fields["form_data"] = formData
		h.extractCredentials(formData, fields)
	}
	if len(files) > 0 {
		fields["uploaded_files"] = files
	}
}

// extractMultipartBoundary extracts the boundary parameter from Content-Type header.
func (h *httpHoneypot) extractMultipartBoundary(contentType string) string {
	parts := strings.Split(contentType, "boundary=")
	if len(parts) < 2 {
		return ""
	}
	return strings.Trim(parts[1], `"`)
}

// parseMultipartParts parses all parts from a multipart reader.
func (h *httpHoneypot) parseMultipartParts(reader *multipart.Reader, fields map[string]interface{}) (map[string]interface{}, []string) {
	formData := make(map[string]interface{})
	files := []string{}

	for {
		part, err := reader.NextPart()
		if err == io.EOF {
			break
		}
		if err != nil {
			// error reading part, skip parsing
			break
		}

		if part.FileName() != "" {
			fileName := part.FileName()
			files = append(files, fileName)
			h.parseMultipartFile(part, formData)
		} else {
			h.parseMultipartField(part, formData)
		}
	}

	return formData, files
}

// parseMultipartFile parses a file upload part.
func (h *httpHoneypot) parseMultipartFile(part *multipart.Part, formData map[string]interface{}) {
	partName := part.FormName()
	fileName := part.FileName()

	fileContent := make([]byte, MaxFilePreviewSize)
	n, _ := part.Read(fileContent)
	if n > 0 {
		formData[partName+"_file"] = map[string]interface{}{
			"filename": fileName,
			"size":     n,
			"preview":  string(fileContent[:n]),
		}
	}
}

// parseMultipartField parses a form field part.
func (h *httpHoneypot) parseMultipartField(part *multipart.Part, formData map[string]interface{}) {
	partName := part.FormName()
	fieldValue, err := io.ReadAll(part)
	if err == nil {
		formData[partName] = string(fieldValue)
	}
}

// parseFormURLEncoded parses an application/x-www-form-urlencoded request body.
func (h *httpHoneypot) parseFormURLEncoded(bodyBytes []byte, fields map[string]interface{}) {
	values, err := url.ParseQuery(string(bodyBytes))
	if err != nil {
		return
	}

	formData := make(map[string]interface{})
	for k, v := range values {
		if len(v) > 0 {
			formData[k] = v[0]
		}
	}

	if len(formData) > 0 {
		fields["form_data"] = formData
		h.extractCredentials(formData, fields)
	}
}

// extractCredentials extracts username and password from form data and sets them
// as top-level fields for consistency with SSH honeypot logging.
func (h *httpHoneypot) extractCredentials(formData map[string]interface{}, fields map[string]interface{}) {
	h.extractUsername(formData, fields)
	h.extractPassword(formData, fields)
}

// extractUsername extracts username from form data.
func (h *httpHoneypot) extractUsername(formData map[string]interface{}, fields map[string]interface{}) {
	h.extractField(formData, fields, UsernameFieldNames, "username")
}

// extractPassword extracts password from form data.
func (h *httpHoneypot) extractPassword(formData map[string]interface{}, fields map[string]interface{}) {
	h.extractField(formData, fields, PasswordFieldNames, "password")
}

// extractField extracts a field value from form data using a list of possible field names.
func (h *httpHoneypot) extractField(formData map[string]interface{}, fields map[string]interface{}, fieldNames []string, fieldKey string) {
	for _, fieldName := range fieldNames {
		if val, ok := formData[fieldName]; ok {
			if strVal, ok := val.(string); ok && strVal != "" {
				fields[fieldKey] = strVal
				return
			}
		}
	}
}

// parseJSON parses JSON body and extracts username/password if present.
func (h *httpHoneypot) parseJSON(bodyBytes []byte, fields map[string]interface{}) {
	var jsonData map[string]interface{}
	if err := json.Unmarshal(bodyBytes, &jsonData); err == nil {
		h.extractCredentials(jsonData, fields)
	}
	h.logBodyAsString(bodyBytes, fields)
}

// recordHTTPMetrics records HTTP-specific metrics.
func (h *httpHoneypot) recordHTTPMetrics(e types.LogEvent) {
	if e.Fields == nil {
		return
	}

	// Record HTTP method
	if method, ok := e.Fields["method"].(string); ok && method != "" && h.httpMethods != nil {
		h.httpMethods.WithLabelValues(metrics.SanitizeUTF8(method)).Inc()
	}
}

func looksLikeBearerToken(token string) bool {
	// JWT (header.payload.signature)
	if strings.Count(token, ".") == 2 {
		return true
	}

	// Common real-world token prefixes
	prefixes := []string{
		"ghp_",        // GitHub
		"github_pat_", // GitHub fine-grained
		"glpat-",      // GitLab
		"AKIA",        // AWS access key
		"ya29.",       // Google OAuth
	}

	for _, p := range prefixes {
		if strings.HasPrefix(token, p) {
			return true
		}
	}

	// Long random-looking tokens
	if len(token) >= 32 {
		return true
	}

	return false
}

func (h *httpHoneypot) GetScores(db *database.Database, interval string) honeypot.ScoreMap {
	// get scores for Authorization headers where the value starts with "Bearer " or "Basic "
	rows, err := db.DB.Query(fmt.Sprintf(`
	SELECT remote_addr, COUNT(*) as authorization_count
	FROM honeypot_events
	WHERE type = 'http'
	AND fields.authorization IS NOT NULL
	AND (fields.authorization LIKE 'Bearer %%' OR fields.authorization LIKE 'Basic %%')
	AND time >= now() - INTERVAL %s
	GROUP BY remote_addr`, interval))
	if err != nil {
		return honeypot.ScoreMap{}
	}
	defer rows.Close()

	scoresAuthorization := honeypot.ScoreMap{}
	for rows.Next() {
		var ip string
		var authorizationCount uint
		err := rows.Scan(&ip, &authorizationCount)
		if err != nil {
			return honeypot.ScoreMap{}
		}
		scoresAuthorization[ip] = honeypot.Score{Score: authorizationCount * 100, Tags: []types.Tag{types.TagAuthAttempt}}
	}

	// get scores for malicious payloads fields contain "wget" or "curl"
	rows, err = db.DB.Query(fmt.Sprintf(`
	SELECT remote_addr, COUNT(*) as malicious_count
	FROM honeypot_events
	WHERE type = 'http'
	AND (fields.body ILIKE '%%wget%%' OR fields.body ILIKE '%%curl%%'
	OR fields.form_data::TEXT ILIKE '%%wget%%' OR fields.form_data::TEXT ILIKE '%%curl%%')
	AND time >= now() - INTERVAL %s
	GROUP BY remote_addr`, interval))
	if err != nil {
		return honeypot.ScoreMap{}
	}
	defer rows.Close()

	scoresMalicious := honeypot.ScoreMap{}
	for rows.Next() {
		var ip string
		var maliciousCount uint
		err := rows.Scan(&ip, &maliciousCount)
		if err != nil {
			return honeypot.ScoreMap{}
		}
		scoresMalicious[ip] = honeypot.Score{Score: maliciousCount * 200, Tags: []types.Tag{types.TagMalware}}
	}

	// merge scores for uri enumerations
	rows, err = db.DB.Query(fmt.Sprintf(`
	SELECT remote_addr, COUNT(*) as uri_enumeration_count
	FROM honeypot_events
	WHERE type = 'http'
	AND (fields.uri ILIKE '%%/.env%%' OR fields.uri ILIKE '%%/.git%%')
	AND time >= now() - INTERVAL %s
	GROUP BY remote_addr`, interval))
	if err != nil {
		return honeypot.ScoreMap{}
	}
	defer rows.Close()

	scoresURIEnumeration := honeypot.ScoreMap{}
	for rows.Next() {
		var ip string
		var uriEnumerationCount uint
		err := rows.Scan(&ip, &uriEnumerationCount)
		if err != nil {
			return honeypot.ScoreMap{}
		}
		scoresURIEnumeration[ip] = honeypot.Score{Score: uriEnumerationCount * 150, Tags: []types.Tag{types.TagInfoStealing}}
	}

	// merge scores
	return honeypot.MergeScores(scoresAuthorization, scoresMalicious, scoresURIEnumeration)
}

func (h *httpHoneypot) Ports() []uint16 {
	ports := append([]uint16{}, h.config.HTTPPorts...)
	ports = append(ports, h.config.HTTPSPorts...)
	return ports
}