package server // ___ ____ ___ ___ // \ \ / / | _ | __| \ \ / / || | __ || || _ | // \ \/ / |___ | |__ \ \/ / || |___ || ||___| // \ / | _ | _ | \ / || __ | || ||\\ // \/ |___ |___ | \/ || ____| || || \\ // Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License // Repo: https://git.pbiernat.dev/golang/vegvisir import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "os" "os/signal" "regexp" "strings" "time" "vegvisir/config" "github.com/valyala/fasthttp" ) const ( Version = "0.1" Name = "Vegvisir/" + Version ) type Server struct { Config config.Config cFilePath string foundBackend *config.Backend foundRoute *config.Route } func NewServer(cPath string) *Server { return &Server{ cFilePath: cPath, } } func (s *Server) Run() { if err := s.loadConfig(); err != nil { log.Fatalln("Unable to find config file: ", s.cFilePath, err) } go func() { serverAddress := s.Config.Server.Address + ":" + fmt.Sprint(s.Config.Server.Port) if err := fasthttp.ListenAndServe(serverAddress, s.mainHandler); err != nil { log.Fatalf("Server panic! Error message: %s", err) } }() log.Println("Server started") // Wait for an interrupt interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt, os.Kill) <-interrupt log.Println("SIGKILL or SIGINT caught, shutting down...") // Attempt a graceful shutdown ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s.Shutdown(ctx) log.Println("Server shutdown successfully.") } func (s *Server) Shutdown(ctx context.Context) { // TODO: wait for all connections to finish log.Println("Shuting down finished") } func (s *Server) loadConfig() error { if _, err := os.Stat(s.cFilePath); err != nil { return err } data, err := ioutil.ReadFile(s.cFilePath) if err != nil { return err } err = json.Unmarshal(data, &s.Config) if err != nil { return err } return nil } func (s *Server) mainHandler(ctx *fasthttp.RequestCtx) { ctx.Response.Header.Add(fasthttp.HeaderServer, Name) // move all below logic to concrete handler or sth....? reqURI, sReqURI, sReqMethod := ctx.RequestURI(), string(ctx.RequestURI()), string(ctx.Method()) log.Println("Incomming request:", sReqMethod, sReqURI) found, rgxp := s.findBackendAndRouteByRequestURI(reqURI) if !found { // FIXME: return 404/5xx error in repsonse, maybe define it in Backend config? ctx.Response.SetStatusCode(fasthttp.StatusNotFound) return } bckReqURI := s.buildBackendTargetURI(*rgxp, sReqURI) // prepare to send request to backend - separate bckReq := fasthttp.AcquireRequest() bckResp := fasthttp.AcquireResponse() defer fasthttp.ReleaseRequest(bckReq) defer fasthttp.ReleaseResponse(bckResp) // copy headers from backend response and prepare request for backend - separate bckReq.SetRequestURI(bckReqURI) bckReq.Header.SetMethod(sReqMethod) fasthttp.Do(bckReq, bckResp) ctx.Response.Header.SetBytesV(fasthttp.HeaderContentType, bckResp.Header.ContentType()) ctx.SetBody(bckResp.Body()) } func (s *Server) findBackendAndRouteByRequestURI(uri []byte) (bool, *regexp.Regexp) { for _, bck := range s.Config.Backends { if !strings.Contains(string(uri), bck.PrefixUrl) { continue } s.foundBackend = &bck // possibly errorneus... for _, r := range bck.Routes { rgxp := regexp.MustCompile(fmt.Sprintf("%s%s", bck.PrefixUrl, r.Pattern)) // fmt.Println("pattern: ", bck.PrefixUrl+r.Pattern, rgxp.Match(uri)) if rgxp.Match(uri) { s.foundBackend = &bck // 100% safe here s.foundRoute = &r return true, rgxp } } } return false, nil } func (s *Server) buildBackendTargetURI(rgxp regexp.Regexp, uri string) string { baseURI := s.foundBackend.BackendAddress url := rgxp.ReplaceAllString(uri, s.foundRoute.Target) // if !strings.HasSuffix(baseURI, "/") { // baseURI += "/" // } log.Println("Send req to backend url: ", baseURI+url) return baseURI + url }