package module import ( "context" "encoding/json" "errors" "fmt" "log/slog" "net" "net/http" "github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/processor" ) type HTTPServer struct { config config.ModuleConfig Port uint16 ctx context.Context router common.RouteIO logger *slog.Logger cancel context.CancelFunc } type ResponseIOError struct { Index int `json:"index"` OutputError *string `json:"outputError"` ProcessError *string `json:"processError"` InputError *string `json:"inputError"` } type IOResponseData struct { IOErrors []ResponseIOError `json:"ioErrors"` Message string `json:"message"` Status string `json:"status"` } type httpServerContextKey string type HTTPServerResponseWriter struct { http.ResponseWriter done bool } func (hsrw *HTTPServerResponseWriter) WriteHeader(status int) { hsrw.done = true hsrw.ResponseWriter.WriteHeader(status) } func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) { hsrw.done = true return hsrw.ResponseWriter.Write(data) } func init() { RegisterModule(ModuleRegistration{ Type: "http.server", New: func(config config.ModuleConfig) (common.Module, error) { params := config.Params portNum, err := params.GetInt("port") if err != nil { return nil, fmt.Errorf("http.server port error: %w", err) } return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil }, }) } func (hs *HTTPServer) Id() string { return hs.config.Id } func (hs *HTTPServer) Type() string { return hs.config.Type } func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { responseWriter := HTTPServerResponseWriter{ResponseWriter: w} response := IOResponseData{ Message: "routing successful", Status: "ok", } if hs.router != nil { inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter) senderAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) if err == nil { inputContext = context.WithValue(inputContext, common.SenderContextKey, senderAddr) } aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r) if !responseWriter.done { if aRouteFound { if routingErrors != nil { w.WriteHeader(http.StatusInternalServerError) response.Status = "error" response.Message = "routing failed" response.IOErrors = []ResponseIOError{} for _, responseIOError := range routingErrors { errorToAdd := ResponseIOError{ Index: responseIOError.Index, } if responseIOError.InputError != nil { errorMsg := responseIOError.InputError.Error() errorToAdd.InputError = &errorMsg } if responseIOError.ProcessError != nil { errorMsg := responseIOError.ProcessError.Error() errorToAdd.ProcessError = &errorMsg } if responseIOError.OutputError != nil { errorMsg := responseIOError.OutputError.Error() errorToAdd.OutputError = &errorMsg } response.IOErrors = append(response.IOErrors, errorToAdd) } json.NewEncoder(w).Encode(response) return } else { w.WriteHeader(http.StatusOK) response.Message = "routing successful" json.NewEncoder(w).Encode(response) return } } else { w.WriteHeader(http.StatusNotFound) response.Status = "error" response.Message = "no matching routes found" json.NewEncoder(w).Encode(response) return } } } else { w.WriteHeader(http.StatusInternalServerError) response.Message = "no router registered" response.Status = "error" json.NewEncoder(w).Encode(response) return } } func (hs *HTTPServer) Start(ctx context.Context) error { hs.logger.Debug("running") router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) if !ok { return errors.New("http.server unable to get router from context") } hs.router = router moduleContext, cancel := context.WithCancel(ctx) hs.ctx = moduleContext hs.cancel = cancel httpServer := &http.Server{ Addr: fmt.Sprintf(":%d", hs.Port), Handler: hs, } go func() { <-hs.ctx.Done() httpServer.Close() }() err := httpServer.ListenAndServe() // TODO(jwetzell): handle server closed error differently if err != nil { if err.Error() != "http: Server closed" { return err } } <-hs.ctx.Done() hs.logger.Debug("done") return nil } func (hs *HTTPServer) Output(ctx context.Context, payload any) error { responseWriter, ok := ctx.Value(httpServerContextKey("responseWriter")).(*HTTPServerResponseWriter) if !ok { return errors.New("http.server output must originate from an http.server input") } payloadResponse, ok := common.GetAnyAs[processor.HTTPResponse](payload) if !ok { return errors.New("http.server is only able to output HTTPResponse") } if responseWriter.done { return errors.New("http.server response writer has already been written to") } responseWriter.WriteHeader(payloadResponse.Status) responseWriter.Write(payloadResponse.Body) return nil } func (hs *HTTPServer) Stop() { hs.cancel() } func (hs *HTTPServer) Get(key string) (any, error) { return nil, errors.New("http.server does not support Get") }