// ___ ____ ___ ___ // \ \ / / | _ | __| \ \ / / || | __ || || _ | // \ \/ / |___ | |__ \ \/ / || |___ || ||___| // \ / | _ | _ | \ / || __ | || ||\\ // \/ |___ |___ | \/ || ____| || || \\ // // Copyright (c) 2021 Piotr Biernat. https://pbiernat.dev. MIT License // Repo: https://git.pbiernat.dev/golang/vegvisir package server import ( "context" "fmt" "log" "os" "os/signal" "time" "vegvisir/pkg/cache" "vegvisir/pkg/config" "github.com/valyala/fasthttp" ) const ( ServerVersion = "0.1-dev" ServerName = "Vegvisir/" + ServerVersion ) type Server struct { config *config.Config router *Router respCM cache.ResponseCacheManager // Redis response cache } func NewServer(cPath string) *Server { config := config.New(cPath) if err := config.Load(); err != nil { log.Fatalln("Unable to find config file: ", cPath, err) } datastore := cache.GetCacheDatastore(config.Cache) return &Server{ config: config, router: NewRouter(config, *datastore, config.Cache.RouteTtl), respCM: cache.NewResponseCacheManager(*datastore, config.Cache.ResponseTtl), } } func (s *Server) Run() { 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) mainHandler(ctx *fasthttp.RequestCtx) { // http := client.NewHttpClient(ctx) // move all below logic to concrete handler or sth.... reqUrl, sReqUrl, sReqMethod := ctx.RequestURI(), string(ctx.RequestURI()), string(ctx.Method()) log.Println("Incoming request:", sReqMethod, sReqUrl) found, route := s.router.FindByRequestURL(reqUrl) if !found { // FIXME: return 404/5xx error in repsonse, maybe define it in Backend config? ctx.SetStatusCode(fasthttp.StatusNotFound) return } // handle response caching if ok, data := s.respCM.Load(sReqUrl); ok { log.Println("Read resp from cache: ", route.TargetUrl) // copy headers and body from cache ctx.Response.Header.DisableNormalizing() for key, value := range data.Headers { ctx.Response.Header.Set(key, value) } ctx.SetBody([]byte(data.Body)) } else { log.Println("Send req to backend url: ", route.TargetUrl) // 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(route.TargetUrl) bckReq.Header.SetMethod(sReqMethod) err := fasthttp.Do(bckReq, bckResp) if err != nil { ctx.SetStatusCode(fasthttp.StatusInternalServerError) return } headers := make(cache.Headers) // rewrite headers from backend to gateway response bckResp.Header.Set(fasthttp.HeaderServer, ServerName) bckResp.Header.Del(fasthttp.HeaderXPoweredBy) ctx.Response.Header.DisableNormalizing() bckResp.Header.VisitAll(func(key, value []byte) { headers[string(key)] = string(value) ctx.Response.Header.SetBytesKV(key, value) }) // ctx.SetStatusCode(bckResp.StatusCode()) ctx.SetBody(bckResp.Body()) // save response to cache respCache := cache.ResponseCache{ URL: sReqUrl, Body: string(bckResp.Body()), Headers: headers, } // FIXME: prepare resp cache struct in respCM.Save method or other service... s.respCM.Save(sReqUrl, respCache) } }