mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
218 lines
5.3 KiB
Go
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)
|
|
}
|