Files
beelzebub/protocols/strategies/HTTP/http.go
Bryan Nolen d677cd20b9 Feature: Enhance Performance, Logging & Stability: Precompile Regex, Command Matching, Golang 1.24, History Cleanup & memLimitMiB Flag. (#182)
* Feat: Add support for logging which "command" was matched for SSH and HTTP strategies.

* Feat: Convert to precompiling regexp at config load time. This allows for errors to be presented to the user during startup, and provides better performance for complex regexp.

* Feat:Bump Golang version to latest stable 1.24

* Feat: Add a cleanup routine for HistoryStore, default TTL for events is 1 hour since last interaction.

* Feat: Add new command line flag "memLimitMiB" with a default value of 100.

---------

Signed-off-by: Bryan Nolen <bryan@arc.net.au>
Signed-off-by: Mario Candela <mario.candela.personal@gmail.com>
Co-authored-by: Mario Candela <mario.candela.personal@gmail.com>
2025-03-23 19:16:34 +01:00

180 lines
5.3 KiB
Go

package HTTP
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/mariocandela/beelzebub/v3/parser"
"github.com/mariocandela/beelzebub/v3/plugins"
"github.com/mariocandela/beelzebub/v3/tracer"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
type HTTPStrategy struct{}
type httpResponse struct {
StatusCode int
Headers []string
Body string
}
func (httpStrategy HTTPStrategy) Init(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer) error {
serverMux := http.NewServeMux()
serverMux.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) {
var matched bool
var resp httpResponse
var err error
for _, command := range servConf.Commands {
var err error
matched = command.Regex.MatchString(request.RequestURI)
if matched {
resp, err = buildHTTPResponse(servConf, tr, command, request)
if err != nil {
log.Errorf("error building http response: %s: %v", request.RequestURI, err)
resp.StatusCode = 500
resp.Body = "500 Internal Server Error"
}
break
}
}
// If none of the main commands matched, and we have a fallback command configured, process it here.
// The regexp is ignored for fallback commands, as they are catch-all for any request.
if !matched {
command := servConf.FallbackCommand
if command.Handler != "" || command.Plugin != "" {
resp, err = buildHTTPResponse(servConf, tr, command, request)
if err != nil {
log.Errorf("error building http response: %s: %v", request.RequestURI, err)
resp.StatusCode = 500
resp.Body = "500 Internal Server Error"
}
}
}
setResponseHeaders(responseWriter, resp.Headers, resp.StatusCode)
fmt.Fprint(responseWriter, resp.Body)
})
go func() {
var err error
// Launch a TLS supporting server if we are supplied a TLS Key and Certificate.
// If relative paths are supplied, they are relative to the CWD of the binary.
// The can be self-signed, only the client will validate this (or not).
if servConf.TLSKeyPath != "" && servConf.TLSCertPath != "" {
err = http.ListenAndServeTLS(servConf.Address, servConf.TLSCertPath, servConf.TLSKeyPath, serverMux)
} else {
err = http.ListenAndServe(servConf.Address, serverMux)
}
if err != nil {
log.Errorf("error during init HTTP Protocol: %v", err)
return
}
}()
log.WithFields(log.Fields{
"port": servConf.Address,
"commands": len(servConf.Commands),
}).Infof("Init service: %s", servConf.Description)
return nil
}
func buildHTTPResponse(servConf parser.BeelzebubServiceConfiguration, tr tracer.Tracer, command parser.Command, request *http.Request) (httpResponse, error) {
resp := httpResponse{
Body: command.Handler,
Headers: command.Headers,
StatusCode: command.StatusCode,
}
traceRequest(request, tr, command, servConf.Description)
if command.Plugin == plugins.LLMPluginName {
llmProvider, err := plugins.FromStringToLLMProvider(servConf.Plugin.LLMProvider)
if err != nil {
log.Errorf("error: %v", err)
resp.Body = "404 Not Found!"
return resp, err
}
llmHoneypot := plugins.LLMHoneypot{
Histories: make([]plugins.Message, 0),
OpenAIKey: servConf.Plugin.OpenAISecretKey,
Protocol: tracer.HTTP,
Host: servConf.Plugin.Host,
Model: servConf.Plugin.LLMModel,
Provider: llmProvider,
CustomPrompt: servConf.Plugin.Prompt,
}
llmHoneypotInstance := plugins.InitLLMHoneypot(llmHoneypot)
command := fmt.Sprintf("%s %s", request.Method, request.RequestURI)
completions, err := llmHoneypotInstance.ExecuteModel(command)
if err != nil {
resp.Body = "404 Not Found!"
return resp, fmt.Errorf("ExecuteModel error: %s, %v", command, err)
}
resp.Body = completions
}
return resp, nil
}
func traceRequest(request *http.Request, tr tracer.Tracer, command parser.Command, HoneypotDescription string) {
bodyBytes, err := io.ReadAll(request.Body)
body := ""
if err == nil {
body = string(bodyBytes)
}
host, port, _ := net.SplitHostPort(request.RemoteAddr)
event := tracer.Event{
Msg: "HTTP New request",
RequestURI: request.RequestURI,
Protocol: tracer.HTTP.String(),
HTTPMethod: request.Method,
Body: body,
HostHTTPRequest: request.Host,
UserAgent: request.UserAgent(),
Cookies: mapCookiesToString(request.Cookies()),
Headers: request.Header,
Status: tracer.Stateless.String(),
RemoteAddr: request.RemoteAddr,
SourceIp: host,
SourcePort: port,
ID: uuid.New().String(),
Description: HoneypotDescription,
Handler: command.Name,
}
// Capture the TLS details from the request, if provided.
if request.TLS != nil {
event.Msg = "HTTPS New Request"
event.TLSServerName = request.TLS.ServerName
}
tr.TraceEvent(event)
}
func mapCookiesToString(cookies []*http.Cookie) string {
cookiesString := ""
for _, cookie := range cookies {
cookiesString += cookie.String()
}
return cookiesString
}
func setResponseHeaders(responseWriter http.ResponseWriter, headers []string, statusCode int) {
for _, headerStr := range headers {
keyValue := strings.Split(headerStr, ":")
if len(keyValue) > 1 {
responseWriter.Header().Add(keyValue[0], keyValue[1])
}
}
// http.StatusText(statusCode): empty string if the code is unknown.
if len(http.StatusText(statusCode)) > 0 {
responseWriter.WriteHeader(statusCode)
}
}