package servlet import ( "encoding/json" "fmt" "geniuscartel.xyz/vinegar/vinegarUtil" "log" "net" "net/http" "regexp" "strconv" "time" ) const ( defaultLruSize = int64(1024 * 1024 * 50) ContentTypeHeaderKey = "Content-Type" ContentEncodingHeaderKey = "Content-Encoding" AcceptEncodingHeaderKey = "Accept-Encoding" ) type ErrorResponse struct { Code int Message string } type ( // VinegarWebServlet is the main server struct that handles HTTP requests and routing. // It contains the following key fields: // //Port - The TCP port to listen on for incoming requests. Should be a string like ":8080". // //Router - A VinegarWebRouter instance that handles routing requests to handlers. // //ErrorRoutes - A map of status code to TemplateRoute for custom error pages. // //interrupts - A channel used to signal shutdown/interrupts to the server. // //errors - A channel that the server writes errors to. // //The main methods are: // //ServeHTTP - Handles all incoming requests. Calls Router.RouteRequest to handle routing. // //AddErrorRoute - Adds a custom error route for the given status code. // //Start - Starts the HTTP server listening on Port. Returns any errors. // //Shutdown - Shuts down the server by sending a signal on interrupts. // //SendError - Helper to send an error response with custom status code and error template. // //Typical usage: // // srv := NewServlet(":8080") // srv.AddErrorRoute(404, notFoundRoute) // srv.Router.AddRoute(someRoute) // srv.Start() // // ... // srv.Shutdown() VinegarWebServlet struct { Port string Router VinegarWebRouter // ErrorRoutes is a map that stores TemplateRoute pointers // for different HTTP status code error pages. // // It gets populated when custom error routes are defined via AddErrorRoute. // For example: // // err404 := NewTemplateRoute(/*...*/) // srv.AddErrorRoute(404, err404) // // This will add err404 to ErrorRoutes mapped to 404. // // Routes can then be rendered like: // // tmpl, exists := s.ErrorRoutes[404] // if exists { // // render tmpl // } // // So ErrorRoutes provides a lookup from status code // to the associated custom error route. // // It allows easily rendering custom error pages // for different status codes. ErrorRoutes map[int]*TemplateRoute interrupts chan struct{} errors chan error } VinegarHandlerFunction func(w http.ResponseWriter, req *http.Request) ) // NewServlet creates and initializes a new VinegarWebServlet instance. // // It accepts the TCP port to listen on as a string parameter. // // It initializes an empty ErrorRoutes map to store custom error routes. // // It creates an interrupts channel to handle shutdown signals. // // It returns a pointer to the initialized VinegarWebServlet struct. // // Example usage: // // port := ":8080" // srv := NewServlet(port) func NewServlet(port string) *VinegarWebServlet { errs := make(map[int]*TemplateRoute) srv := VinegarWebServlet{Port: port, ErrorRoutes: errs, interrupts: make(chan struct{}, 500)} return &srv } // NewServletRoute creates a new VinegarWebRoute instance. // // It accepts a routePattern string and handleFunc function as parameters: // // - routePattern is a regex string that will be compiled into the route Pattern. // // - handleFunc is the handler function to call when the route matches. // // It compiles the routePattern into a regexp.Pattern. // // It creates a VinegarWebRoute struct initialized with the Pattern, // the handler function, and a new LRU cache. // // The returned VinegarWebRoute pointer can be added to a router to handle requests // that match the routePattern. func NewServletRoute(routePattern string, handleFunc VinegarHandlerFunction) *VinegarWebRoute { pattern := regexp.MustCompile(routePattern) route := VinegarWebRoute{Pattern: pattern, Handler: handleFunc, Cache: vinegarUtil.NewLRU(defaultLruSize)} return &route } // AddErrorRoute adds a custom error route for the given status code. // // It accepts two parameters: // // - code: The HTTP status code this error route should handle. // // - route: A pointer to the TemplateRoute to use for that status code. // // It adds the route to the ErrorRoutes map, mapped to the status code. // // This allows custom error handling routes to be defined for different // HTTP status codes. The ErrorRoutes map can then be checked to see if // a custom error template has been defined for a given code. // // For example: // // err404 := NewTemplateRoute(...) // srv.AddErrorRoute(404, err404) // // // Later... // src.SendError(404) func (s *VinegarWebServlet) AddErrorRoute(code int, route *TemplateRoute) { route.Announce() s.ErrorRoutes[code] = route } // ServeHTTP handles all incoming HTTP requests. // // It is called by the net/http server whenever a new request comes in. // // It takes an http.ResponseWriter and *http.Request as parameters. // // It calls the router's RouteRequest method to try to find a matching // route and execute the handler. // // If routing fails, it returns a 404 Not Found response. // // This is the main request handler that handles routing and responding // to all incoming requests. The VinegarWebServlet implements the // http.Handler interface so it can be used by the net/http server. // func (s *VinegarWebServlet) ServeHTTP(w http.ResponseWriter, req *http.Request) { err := s.Router.RouteRequest(w, req) if err != nil { http.NotFound(w, req) } } // Start starts the VinegarWebServlet HTTP server listening on the configured port. // // It first checks that there is at least one route configured, otherwise it // logs a fatal error and exits. // // It then calls net.Listen() to listen on the TCP port. Any errors are returned. // // It spawns a new goroutine to handle serving HTTP requests via http.Serve(). // Any errors from http.Serve() are sent on the s.errors channel. // // The interrupts channel in VinegarWebServlet is used to gracefully shutdown the HTTP server when needed // If VinegarWebServlet.interrupts receives a signal via VinegarWebServlet.Shutdown(), the listener is closed and the server exits. // // Any errors from starting the listener or serving requests are returned. // func (s *VinegarWebServlet) Start() error { if len(s.Router.Routes) < 1 { log.Fatal("No routes found for server. Nothing to listen and serve.") } l, listenErr := net.Listen("tcp", s.Port) if listenErr != nil { return listenErr } go func() { err := http.Serve(l, s) if err != nil { s.errors <- err } }() log.Printf("Listening on [%s]\n", s.Port) running := true for running { select { case err := <-s.errors: log.Printf("server on port %s failed: %v", s.Port, err) case <-s.interrupts: err := l.Close() if err != nil { return err } running = false default: time.Sleep(5) } } return nil } // Shutdown gracefully shuts down the VinegarWebServlet HTTP server. // // It sends a signal on the s.interrupts channel, which signals the // server's main loop to exit and stop listening/serving. // // This allows cleanly shutting down the HTTP server on demand without // having to force kill any processes. // //Currently, this does not wait for outstanding responses to finish. // // Typical usage: // // srv := NewServlet(":8080") // // ...start server // // // Later, to shutdown // srv.Shutdown() // func (s *VinegarWebServlet) Shutdown() { s.interrupts <- struct{}{} } // Announce logs a message when a VinegarWebRoute is added to the router. // // It does not accept any parameters. // // It prints a log message containing the route's URL pattern string. // This indicates a new route was added. // // For example: // // route := NewServletRoute("/users", handler) // router.AddRoute(route) // // // Prints: // // Added route for [/users] // func (r *VinegarWebRoute) Announce() { log.Printf("Added route for [%s]\n", r.Pattern.String()) } // SendError sends an error response to the client with a custom status code // and error message. // // It takes the following parameters: // // w - http.ResponseWriter to write the response to // req - *http.Request that is causing the error // code - int HTTP status code for the error response // msg - string message describing the error // aErr - error instance with more details on the error // // It first logs the error details. // // It then checks if there is a custom ErrorRoute defined for the status code. // If so, it renders that template and writes it to the response. // // If not, it calls http.Error to send a generic error response. // // In both cases it sets the appropriate HTTP status code. // // This allows customizing error pages for different status codes, // while falling back to default Go error handling. func (s *VinegarWebServlet) SendError(w http.ResponseWriter, req *http.Request, code int, msg string, aErr error) { log.Printf("[%d][%s]. Rendering template for code %d with message: %s\n", code, req.URL.Path, code, msg) out, _ := json.Marshal(aErr) log.Println(string(out)) tmpl, exists := s.ErrorRoutes[code] if exists { err := tmpl.TemplateManager.AddMixin("code", strconv.Itoa(code)) if err != nil { http.Error(w, msg, code) return } err = tmpl.TemplateManager.AddMixin("msg", msg) if err != nil { http.Error(w, msg, code) return } w.WriteHeader(code) bitties, err := tmpl.TemplateManager.RenderTemplate(fmt.Sprintf("%d.html", code)) if err != nil { http.Error(w, msg, code) return } _, err = w.Write([]byte(bitties)) if err != nil { http.Error(w, msg, code) return } return } http.Error(w, msg, code) return }