package scenario import ( "context" "errors" "fmt" "io" "mime" "net" "net/http" "os" "path/filepath" "strings" "sync" "github.com/cucumber/godog" "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/mholt/archives" ) type server struct { srv *echo.Echo req *http.Request bodyCopy []byte errChan chan error eventBody []byte eventMu sync.Mutex } func newServer(ctx context.Context, workdir string) (*server, error) { srv := echo.New() srv.HideBanner = true srv.HidePort = true s := &server{ srv: srv, errChan: make(chan error, 1), } wd, err := os.Getwd() if err != nil { return nil, fmt.Errorf("get current directory: %w", err) } webhookErr := func(err error) error { s.errChan <- err return err } webhookHandler := func(c echo.Context) error { s.req = c.Request() body, err := io.ReadAll(s.req.Body) if err != nil { return webhookErr(fmt.Errorf("read request body: %w", err)) } s.bodyCopy = body cd := s.req.Header.Get("Content-Disposition") if cd == "" { return webhookErr(fmt.Errorf("no Content-Disposition header")) } _, params, err := mime.ParseMediaType(cd) if err != nil { return webhookErr(fmt.Errorf("parse Content-Disposition header: %w", err)) } filename, ok := params["filename"] if !ok { filename = uuid.NewString() contentType := s.req.Header.Get("Content-Type") switch contentType { case "application/zip": filename = fmt.Sprintf("%s.zip", filename) case "application/pdf": filename = fmt.Sprintf("%s.pdf", filename) default: return webhookErr(errors.New("no filename in Content-Disposition header")) } } dirPath := fmt.Sprintf("%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace")) // #nosec err = os.MkdirAll(dirPath, 0o755) if err != nil { return webhookErr(fmt.Errorf("create working directory: %w", err)) } fpath := fmt.Sprintf("%s/%s", dirPath, filename) // #nosec file, err := os.Create(fpath) if err != nil { return webhookErr(fmt.Errorf("create file %q: %w", fpath, err)) } defer file.Close() _, err = file.Write(body) if err != nil { return webhookErr(fmt.Errorf("write file %q: %w", fpath, err)) } if s.req.Header.Get("Content-Type") == "application/zip" { var format archives.Zip err = format.Extract(ctx, file, func(ctx context.Context, f archives.FileInfo) error { source, err := f.Open() if err != nil { return fmt.Errorf("open file %q: %w", f.Name(), err) } defer source.Close() targetPath := fmt.Sprintf("%s/%s", dirPath, f.Name()) target, err := os.Create(targetPath) if err != nil { return fmt.Errorf("create file %q: %w", targetPath, err) } defer target.Close() _, err = io.Copy(target, source) if err != nil { return fmt.Errorf("copy file %q: %w", targetPath, err) } return nil }) if err != nil { return webhookErr(err) } } return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK))) } webhookErrorHandler := func(c echo.Context) error { s.req = c.Request() body, err := io.ReadAll(s.req.Body) if err != nil { return webhookErr(fmt.Errorf("read request body: %w", err)) } s.bodyCopy = body return webhookErr(c.String(http.StatusOK, http.StatusText(http.StatusOK))) } srv.POST("/webhook", webhookHandler) srv.PATCH("/webhook", webhookHandler) srv.PUT("/webhook", webhookHandler) srv.POST("/webhook/error", webhookErrorHandler) srv.PATCH("/webhook/error", webhookErrorHandler) srv.PUT("/webhook/error", webhookErrorHandler) webhookEventsHandler := func(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { return c.String(http.StatusInternalServerError, err.Error()) } s.eventMu.Lock() s.eventBody = body s.eventMu.Unlock() return c.String(http.StatusOK, http.StatusText(http.StatusOK)) } srv.POST("/webhook/events", webhookEventsHandler) srv.GET("/static/:path", func(c echo.Context) error { s.req = c.Request() path := c.Param("path") if strings.Contains(path, "teststore") { return c.Attachment(fmt.Sprintf("%s/%s/%s", workdir, s.req.Header.Get("Gotenberg-Trace"), filepath.Base(path)), filepath.Base(path)) } return c.Attachment(fmt.Sprintf("%s/%s", wd, path), filepath.Base(path)) }) srv.GET("/html/:path", func(c echo.Context) error { s.req = c.Request() path := fmt.Sprintf("%s/%s", wd, c.Param("path")) f, err := os.Open(path) if err != nil { return c.String(http.StatusInternalServerError, fmt.Sprintf("open file %q: %s", path, err)) } defer f.Close() b, err := io.ReadAll(f) if err != nil { return c.String(http.StatusInternalServerError, fmt.Sprintf("read file %q: %s", path, err)) } return c.HTML(http.StatusOK, string(b)) }) return s, nil } func (s *server) getEventBody() []byte { s.eventMu.Lock() defer s.eventMu.Unlock() return s.eventBody } func (s *server) start(ctx context.Context) (int, error) { // #nosec ln, err := net.Listen("tcp", "0.0.0.0:0") if err != nil { return 0, fmt.Errorf("create listener: %w", err) } port := ln.Addr().(*net.TCPAddr).Port go func() { s.srv.Listener = ln err = s.srv.Start("") if err != nil && !errors.Is(err, http.ErrServerClosed) { godog.Log(ctx, err.Error()) } }() return port, nil } func (s *server) stop(ctx context.Context) error { close(s.errChan) return s.srv.Shutdown(ctx) }