diff --git a/init.go b/init.go index 1f52d3c0..a4e3246c 100644 --- a/init.go +++ b/init.go @@ -39,6 +39,16 @@ func main() { return } + err = os.MkdirAll(storage.PathProxy, os.ModePerm) + if err != nil && !os.IsExist(err) { + logger.Error(fmt.Errorf("failed to create directory: %v", err)). + AddKeyValue("message", "failed to create directory"). + AddKeyValue("path", storage.PathProxy). + Print() + + return + } + err = setupDependencies() if err != nil { logger.Error(fmt.Errorf("failed to setup dependencies: %v", err)).Print() @@ -51,7 +61,7 @@ func main() { return } - r := router.Create(router.About{ + router := router.NewRouter(types.About{ Version: version, Commit: commit, Date: date, @@ -59,22 +69,16 @@ func main() { OS: runtime.GOOS, Arch: runtime.GOARCH, }) - defer router.Unload() + defer router.Stop() + // Logs url := fmt.Sprintf("http://%s", config.Current.Host) - fmt.Printf("\n-- Vertex Client :: %s\n\n", url) - logger.Log("Vertex started"). AddKeyValue("url", url). Print() - err = r.Run(fmt.Sprintf(":%s", config.Current.Port)) - if err != nil { - logger.Error(fmt.Errorf("error while starting server: %v", err)).Print() - } - - logger.Log("Vertex stopped").Print() + router.Start(fmt.Sprintf(":%s", config.Current.Port)) } func parseArgs() { diff --git a/pkg/ginutils/logger.go b/pkg/ginutils/logger.go new file mode 100644 index 00000000..537382ab --- /dev/null +++ b/pkg/ginutils/logger.go @@ -0,0 +1,30 @@ +package ginutils + +import ( + "strings" + + "github.com/gin-gonic/gin" + "github.com/vertex-center/vertex/pkg/logger" +) + +func Logger(router string) gin.HandlerFunc { + return gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string { + l := logger.Request(). + AddKeyValue("router", router). + AddKeyValue("method", params.Method). + AddKeyValue("status", params.StatusCode). + AddKeyValue("path", params.Path). + AddKeyValue("latency", params.Latency). + AddKeyValue("ip", params.ClientIP). + AddKeyValue("size", params.BodySize) + + if params.ErrorMessage != "" { + err, _ := strings.CutSuffix(params.ErrorMessage, "\n") + l.AddKeyValue("error", err) + } + + l.PrintInExternalFiles() + + return l.String() + }) +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 57af2fe9..ef5cc1a2 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -21,6 +21,7 @@ import ( const ( PathClient = "live/client" PathPackages = "live/packages" + PathProxy = "live/proxy" PathInstances = "live/instances" PathServices = "live/services" PathUpdates = "live/updates" diff --git a/repository/proxy_fs.go b/repository/proxy_fs.go new file mode 100644 index 00000000..b8daf946 --- /dev/null +++ b/repository/proxy_fs.go @@ -0,0 +1,82 @@ +package repository + +import ( + "encoding/json" + "errors" + "os" + "path" + + "github.com/google/uuid" + "github.com/vertex-center/vertex/pkg/logger" + "github.com/vertex-center/vertex/pkg/storage" + "github.com/vertex-center/vertex/types" +) + +type ProxyFSRepository struct { + redirects types.ProxyRedirects + proxyPath string +} + +type ProxyRepositoryParams struct { + proxyPath string +} + +func NewProxyFSRepository(params *ProxyRepositoryParams) ProxyFSRepository { + if params == nil { + params = &ProxyRepositoryParams{} + } + if params.proxyPath == "" { + params.proxyPath = storage.PathProxy + } + + repo := ProxyFSRepository{ + redirects: types.ProxyRedirects{}, + proxyPath: params.proxyPath, + } + repo.read() + + return repo +} + +func (r *ProxyFSRepository) GetRedirects() types.ProxyRedirects { + return r.redirects +} + +func (r *ProxyFSRepository) AddRedirect(id uuid.UUID, redirect types.ProxyRedirect) error { + r.redirects[id] = redirect + return r.write() +} + +func (r *ProxyFSRepository) RemoveRedirect(id uuid.UUID) error { + delete(r.redirects, id) + return r.write() +} + +func (r *ProxyFSRepository) read() { + p := path.Join(r.proxyPath, "redirects.json") + file, err := os.ReadFile(p) + + if errors.Is(err, os.ErrNotExist) { + logger.Log("redirects.json doesn't exists or could not be found").Print() + } else if err != nil { + logger.Error(err).Print() + return + } + + err = json.Unmarshal(file, &r.redirects) + if err != nil { + logger.Error(err).Print() + return + } +} + +func (r *ProxyFSRepository) write() error { + p := path.Join(r.proxyPath, "redirects.json") + + bytes, err := json.MarshalIndent(r.redirects, "", "\t") + if err != nil { + return err + } + + return os.WriteFile(p, bytes, os.ModePerm) +} diff --git a/router/proxy.go b/router/proxy.go new file mode 100644 index 00000000..46e8ff4f --- /dev/null +++ b/router/proxy.go @@ -0,0 +1,71 @@ +package router + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/vertex-center/vertex/types" +) + +func addProxyRoutes(r *gin.RouterGroup) { + r.GET("/redirects", handleGetRedirects) + r.POST("/redirect", handleAddRedirect) + r.DELETE("/redirect/:id", handleRemoveRedirect) +} + +func handleGetRedirects(c *gin.Context) { + redirects := proxyService.GetRedirects() + c.JSON(http.StatusOK, redirects) +} + +type handleAddRedirectBody struct { + Source string `json:"source"` + Target string `json:"target"` +} + +func handleAddRedirect(c *gin.Context) { + var body handleAddRedirectBody + err := c.BindJSON(&body) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, fmt.Errorf("failed to parse body: %v", err)) + return + } + + redirect := types.ProxyRedirect{ + Source: body.Source, + Target: body.Target, + } + + err = proxyService.AddRedirect(redirect) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.Status(http.StatusOK) +} + +func handleRemoveRedirect(c *gin.Context) { + idString := c.Param("id") + if idString == "" { + _ = c.AbortWithError(http.StatusBadRequest, errors.New("failed to get redirection uuid")) + return + } + + id, err := uuid.Parse(idString) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + err = proxyService.RemoveRedirect(id) + if err != nil { + _ = c.AbortWithError(http.StatusInternalServerError, err) + return + } + + c.Status(http.StatusOK) +} diff --git a/router/router.go b/router/router.go index e83902b9..b7d8a250 100644 --- a/router/router.go +++ b/router/router.go @@ -1,21 +1,24 @@ package router import ( + "context" + "errors" "net/http" "os" "os/signal" "path" - "strings" "github.com/gin-contrib/cors" "github.com/gin-contrib/sse" "github.com/gin-contrib/static" "github.com/gin-gonic/gin" - "github.com/vertex-center/vertex-core-golang/router" + "github.com/vertex-center/vertex-core-golang/router/middleware" + "github.com/vertex-center/vertex/pkg/ginutils" "github.com/vertex-center/vertex/pkg/logger" "github.com/vertex-center/vertex/pkg/storage" "github.com/vertex-center/vertex/repository" "github.com/vertex-center/vertex/services" + "github.com/vertex-center/vertex/types" ) var ( @@ -26,47 +29,32 @@ var ( eventInMemoryRepo repository.EventInMemoryRepository packageRepo repository.PackageFSRepository serviceRepo repository.ServiceFSRepository + proxyRepo repository.ProxyFSRepository packageService services.PackageService serviceService services.ServiceService + proxyService services.ProxyService instanceService services.InstanceService updateService services.UpdateDependenciesService ) -type About struct { - Version string `json:"version"` - Commit string `json:"commit"` - Date string `json:"date"` - - OS string `json:"os"` - Arch string `json:"arch"` +type Router struct { + server *http.Server + engine *gin.Engine } -func Create(about About) *gin.Engine { +func NewRouter(about types.About) Router { gin.SetMode(gin.ReleaseMode) - r, api := router.CreateRouter( - cors.Default(), - gin.LoggerWithFormatter(func(params gin.LogFormatterParams) string { - l := logger.Request(). - AddKeyValue("method", params.Method). - AddKeyValue("status", params.StatusCode). - AddKeyValue("path", params.Path). - AddKeyValue("latency", params.Latency). - AddKeyValue("ip", params.ClientIP). - AddKeyValue("size", params.BodySize) - - if params.ErrorMessage != "" { - err, _ := strings.CutSuffix(params.ErrorMessage, "\n") - l.AddKeyValue("error", err) - } - - l.PrintInExternalFiles() - - return l.String() - }), - ) + router := Router{} + + r := gin.New() + r.Use(cors.Default()) + r.Use(ginutils.Logger("MAIN")) + r.Use(gin.Recovery()) + r.Use(middleware.ErrorMiddleware()) r.Use(static.Serve("/", static.LocalFile(path.Join(".", storage.PathClient, "dist"), true))) + r.GET("/ping", handlePing) runnerDockerRepo = repository.NewRunnerDockerRepository() runnerFSRepo = repository.NewRunnerFSRepository() @@ -75,46 +63,89 @@ func Create(about About) *gin.Engine { eventInMemoryRepo = repository.NewEventInMemoryRepository() packageRepo = repository.NewPackageFSRepository(nil) serviceRepo = repository.NewServiceFSRepository(nil) + proxyRepo = repository.NewProxyFSRepository(nil) + proxyService = services.NewProxyService(&proxyRepo) instanceService = services.NewInstanceService(&serviceRepo, &instanceRepo, &runnerDockerRepo, &runnerFSRepo, &instanceLogsRepo, &eventInMemoryRepo) packageService = services.NewPackageService(&packageRepo) serviceService = services.NewServiceService(&serviceRepo) updateService = services.NewUpdateDependenciesService(about.Version) - go func() { - instanceService.StartAll() - }() - - handleSignals() + api := r.Group("/api") + api.GET("/ping", handlePing) + api.GET("/about", func(c *gin.Context) { + c.JSON(http.StatusOK, about) + }) addServicesRoutes(api.Group("/services")) addInstancesRoutes(api.Group("/instances")) addInstanceRoutes(api.Group("/instance/:instance_uuid")) addPackagesRoutes(api.Group("/packages")) + addProxyRoutes(api.Group("/proxy")) addUpdatesRoutes(api.Group("/updates")) - api.GET("/about", func(c *gin.Context) { - c.JSON(http.StatusOK, about) - }) + router.engine = r - return r + return router } -func handleSignals() { +func (r *Router) Start(addr string) { + go func() { + err := proxyService.Start() + if err != nil { + logger.Error(err).Print() + return + } + + instanceService.StartAll() + }() + + r.handleSignals() + + r.server = &http.Server{ + Addr: addr, + Handler: r.engine, + } + + err := r.server.ListenAndServe() + if errors.Is(err, http.ErrServerClosed) { + logger.Log("Vertex closed").Print() + } else if err != nil { + logger.Error(err).Print() + } +} + +func (r *Router) Stop() { + // TODO: Stop() must also stop handleSignals() + + instanceService.StopAll() + + err := r.server.Shutdown(context.Background()) + if err != nil { + logger.Error(err).Print() + return + } + + r.server = nil +} + +func handlePing(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) +} + +func (r *Router) handleSignals() { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c logger.Log("shutdown signal sent").Print() - Unload() + r.Stop() os.Exit(0) }() } -func Unload() { - instanceService.StopAll() -} - func headersSSE(c *gin.Context) { c.Writer.Header().Set("Content-Type", sse.ContentType) c.Writer.Header().Set("Cache-Control", "no-cache") diff --git a/services/proxy.go b/services/proxy.go new file mode 100644 index 00000000..5b29e9b0 --- /dev/null +++ b/services/proxy.go @@ -0,0 +1,124 @@ +package services + +import ( + "context" + "errors" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/vertex-center/vertex-core-golang/router/middleware" + "github.com/vertex-center/vertex/pkg/ginutils" + "github.com/vertex-center/vertex/pkg/logger" + "github.com/vertex-center/vertex/types" +) + +var ( + ErrProxyAlreadyRunning = errors.New("a proxy is already running, cannot start a new one") +) + +type ProxyService struct { + server *http.Server + proxyRepo types.ProxyRepository +} + +func NewProxyService(proxyRepo types.ProxyRepository) ProxyService { + s := ProxyService{ + proxyRepo: proxyRepo, + } + return s +} + +func (s *ProxyService) Start() error { + if s.server != nil { + return ErrProxyAlreadyRunning + } + + r := gin.New() + r.Use(cors.Default()) + r.Use(ginutils.Logger("PROX")) + r.Use(gin.Recovery()) + r.Use(middleware.ErrorMiddleware()) + r.Any("/*path", s.handleProxy) + + s.server = &http.Server{ + Addr: ":80", + Handler: r, + } + + go func() { + err := s.server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error(err).Print() + return + } + }() + + return nil +} + +func (s *ProxyService) Stop() error { + err := s.server.Shutdown(context.Background()) + if err != nil { + return err + } + + s.server = nil + return nil +} + +func (s *ProxyService) GetRedirects() types.ProxyRedirects { + return s.proxyRepo.GetRedirects() +} + +func (s *ProxyService) AddRedirect(redirect types.ProxyRedirect) error { + id := uuid.New() + return s.proxyRepo.AddRedirect(id, redirect) +} + +func (s *ProxyService) RemoveRedirect(id uuid.UUID) error { + return s.proxyRepo.RemoveRedirect(id) +} + +func (s *ProxyService) handleProxy(c *gin.Context) { + host := c.Request.Host + + var redirect *types.ProxyRedirect + for _, r := range s.proxyRepo.GetRedirects() { + if host == r.Source { + redirect = &r + break + } + } + + if redirect == nil { + logger.Warn("this host is not registered in the reverse proxy"). + AddKeyValue("host", host). + Print() + return + } + + target, err := url.Parse(redirect.Target) + if err != nil { + logger.Error(err).Print() + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ErrorHandler = func(w http.ResponseWriter, request *http.Request, err error) { + if err != nil && !errors.Is(err, context.Canceled) { + logger.Error(err).Print() + } + } + proxy.Director = func(request *http.Request) { + request.Header = c.Request.Header + request.Host = target.Host + request.URL.Scheme = target.Scheme + request.URL.Host = target.Host + request.URL.Path = c.Param("path") + } + proxy.ServeHTTP(c.Writer, c.Request) +} diff --git a/types/about.go b/types/about.go new file mode 100644 index 00000000..6342b572 --- /dev/null +++ b/types/about.go @@ -0,0 +1,10 @@ +package types + +type About struct { + Version string `json:"version"` + Commit string `json:"commit"` + Date string `json:"date"` + + OS string `json:"os"` + Arch string `json:"arch"` +} diff --git a/types/proxy.go b/types/proxy.go new file mode 100644 index 00000000..651621fd --- /dev/null +++ b/types/proxy.go @@ -0,0 +1,16 @@ +package types + +import "github.com/google/uuid" + +type ProxyRedirects map[uuid.UUID]ProxyRedirect + +type ProxyRedirect struct { + Source string `json:"source"` + Target string `json:"target"` +} + +type ProxyRepository interface { + GetRedirects() ProxyRedirects + AddRedirect(id uuid.UUID, redirect ProxyRedirect) error + RemoveRedirect(id uuid.UUID) error +}