Files
2026-03-28 19:00:07 +01:00

218 lines
5.3 KiB
Go

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)
}