mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
211 lines
5.0 KiB
Go
211 lines
5.0 KiB
Go
package context
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource"
|
|
"github.com/thecodingmachine/gotenberg/internal/pkg/conf"
|
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xerror"
|
|
"github.com/thecodingmachine/gotenberg/internal/pkg/xlog"
|
|
)
|
|
|
|
// Context extends the default echo.Context.
|
|
type Context struct {
|
|
echo.Context
|
|
logger xlog.Logger
|
|
config conf.Config
|
|
resource resource.Resource
|
|
startTime time.Time
|
|
}
|
|
|
|
// New creates a new Context.
|
|
func New(c echo.Context, logger xlog.Logger, config conf.Config) Context {
|
|
return Context{
|
|
c,
|
|
logger,
|
|
config,
|
|
resource.Resource{},
|
|
time.Now(),
|
|
}
|
|
}
|
|
|
|
/*
|
|
MustCastFromEchoContext cast an echo.Context
|
|
to our custom Context.
|
|
|
|
It panics if casting goes wrong.
|
|
*/
|
|
func MustCastFromEchoContext(c echo.Context) Context {
|
|
const op string = "context.MustCastFromEchoContext"
|
|
ctx, ok := c.(Context)
|
|
if !ok {
|
|
panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op))
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
/*
|
|
XLogger returns the xlog.Logger associated
|
|
with the Context.
|
|
|
|
This method should be used instead of the
|
|
default Logger() method coming from
|
|
the echo.Context.
|
|
*/
|
|
func (ctx Context) XLogger() xlog.Logger {
|
|
return ctx.logger
|
|
}
|
|
|
|
// Config returns the conf.Config associated
|
|
// with the Context.
|
|
func (ctx Context) Config() conf.Config {
|
|
return ctx.config
|
|
}
|
|
|
|
// WithResource creates a resource.Resource and
|
|
// adds it to the Context.
|
|
func (ctx *Context) WithResource(directoryName string) error {
|
|
const op string = "context.Context.WithResource"
|
|
resolver := func() (resource.Resource, error) {
|
|
r, err := resource.New(ctx.logger, directoryName)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
// retrieve custom headers from request.
|
|
for key, value := range ctx.Request().Header {
|
|
r.WithCustomHTTPHeader(key, value[0])
|
|
}
|
|
// retrieve form values from request.
|
|
for _, key := range resource.ArgKeys() {
|
|
r.WithArg(key, ctx.FormValue(string(key)))
|
|
}
|
|
// write form files from request.
|
|
form, err := ctx.MultipartForm()
|
|
if err != nil {
|
|
/*
|
|
(very) special case: one and
|
|
only one file has been sent
|
|
and it is empty.
|
|
*/
|
|
if strings.Contains(err.Error(), io.EOF.Error()) {
|
|
return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err)
|
|
}
|
|
return r, err
|
|
}
|
|
for _, files := range form.File {
|
|
for _, fh := range files {
|
|
in, err := fh.Open()
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
defer in.Close()
|
|
// avoid directory traversal.
|
|
filename := filepath.Base(fh.Filename)
|
|
if err := r.WithFile(filename, in); err != nil {
|
|
return r, err
|
|
}
|
|
}
|
|
}
|
|
return r, nil
|
|
}
|
|
resource, err := resolver()
|
|
ctx.resource = resource
|
|
if err != nil {
|
|
return xerror.New(op, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasResource returns true if the Context
|
|
// has a resource.Resource.
|
|
func (ctx Context) HasResource() bool {
|
|
return !reflect.DeepEqual(ctx.resource, resource.Resource{})
|
|
}
|
|
|
|
/*
|
|
MustResource returns the resource.Resource
|
|
associated with the Context.
|
|
|
|
It panics if no resource.Resource.
|
|
*/
|
|
func (ctx Context) MustResource() resource.Resource {
|
|
const op string = "context.Context.MustResource"
|
|
if !ctx.HasResource() {
|
|
panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op))
|
|
}
|
|
return ctx.resource
|
|
}
|
|
|
|
/*
|
|
LogRequestResult logs the result of a request.
|
|
This method should only be used by a middleware!
|
|
|
|
If an error is given, returns the exact same error.
|
|
*/
|
|
func (ctx Context) LogRequestResult(err error, isDebug bool) error {
|
|
const op string = "context.Context.LogRequestResult"
|
|
req := ctx.Request()
|
|
resp := ctx.Response()
|
|
stopTime := time.Now()
|
|
fields := map[string]interface{}{
|
|
"remote_ip": ctx.RealIP(),
|
|
"host": req.Host,
|
|
"uri": req.RequestURI,
|
|
"method": req.Method,
|
|
"path": path(req),
|
|
"referer": req.Referer(),
|
|
"user_agent": req.UserAgent(),
|
|
"status": resp.Status,
|
|
"latency": lantency(ctx.startTime, stopTime),
|
|
"latency_human": latencyHuman(ctx.startTime, stopTime),
|
|
"bytes_in": bytesIn(req),
|
|
"bytes_out": bytesOut(resp),
|
|
}
|
|
if err != nil {
|
|
ctx.logger.WithFields(fields).ErrorOpf(op, "request failed")
|
|
return err
|
|
}
|
|
if isDebug {
|
|
ctx.logger.WithFields(fields).DebugOpf(op, "request handled")
|
|
return nil
|
|
}
|
|
ctx.logger.WithFields(fields).InfoOpf(op, "request handled")
|
|
return nil
|
|
}
|
|
|
|
func path(r *http.Request) string {
|
|
path := r.URL.Path
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
return path
|
|
}
|
|
|
|
func lantency(startTime time.Time, stopTime time.Time) string {
|
|
return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10)
|
|
}
|
|
|
|
func latencyHuman(startTime time.Time, stopTime time.Time) string {
|
|
return stopTime.Sub(startTime).String()
|
|
}
|
|
|
|
func bytesIn(r *http.Request) string {
|
|
bytesIn := r.Header.Get(echo.HeaderContentLength)
|
|
if bytesIn == "" {
|
|
bytesIn = "0"
|
|
}
|
|
return bytesIn
|
|
}
|
|
|
|
func bytesOut(r *echo.Response) string {
|
|
return strconv.FormatInt(r.Size, 10)
|
|
}
|