|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.blauwelle.com/go/crate/log"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxParseMemory = 16 * 1024 * 1024
|
|
|
|
)
|
|
|
|
|
|
|
|
func newHandler() http.HandlerFunc {
|
|
|
|
return func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
var response Response
|
|
|
|
|
|
|
|
response.Request.TransferEncoding = r.TransferEncoding
|
|
|
|
response.Request.Proto = r.Proto
|
|
|
|
response.Request.Host = r.Host
|
|
|
|
response.Request.Method = r.Method
|
|
|
|
response.Request.URL = r.URL.String()
|
|
|
|
response.Request.RemoteAddr = r.RemoteAddr
|
|
|
|
response.Request.RequestURI = r.RequestURI
|
|
|
|
response.Request.Header = r.Header
|
|
|
|
response.Request.ContentLength = r.ContentLength
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
|
|
|
duration := time.Since(start)
|
|
|
|
code := http.StatusOK
|
|
|
|
var message string
|
|
|
|
|
|
|
|
if r.ContentLength > 0 {
|
|
|
|
if body, err := readBody(ctx, r); err != nil {
|
|
|
|
response.Error = err.Error()
|
|
|
|
} else {
|
|
|
|
response.Request.Body = body
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
rw.Header().Set("Content-Type", "application/json")
|
|
|
|
rw.WriteHeader(code)
|
|
|
|
|
|
|
|
end := time.Now()
|
|
|
|
response.ServeDuration = end.Sub(start).String()
|
|
|
|
encoder := json.NewEncoder(rw)
|
|
|
|
encoder.SetEscapeHTML(true)
|
|
|
|
encoder.SetIndent("", " ")
|
|
|
|
err := encoder.Encode(response)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf(ctx, "json marshal: %s", err.Error())
|
|
|
|
}
|
|
|
|
|
|
|
|
log.WithFields(
|
|
|
|
log.Field("code", code),
|
|
|
|
log.Field("duration", duration.String()),
|
|
|
|
).Info(ctx, message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type Response struct {
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
ServeDuration string `json:"serveDuration"`
|
|
|
|
Request ResponseRequest `json:"request"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type MultipartFormFileInfo struct {
|
|
|
|
MIMEHeader map[string][]string `json:"mimeHeader"`
|
|
|
|
Filename string `json:"filename"`
|
|
|
|
Size int64 `json:"size"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type MultipartForm struct {
|
|
|
|
Values map[string][]string `json:"values"`
|
|
|
|
Files map[string][]MultipartFormFileInfo `json:"files"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ResponseRequest struct {
|
|
|
|
Header http.Header `json:"header"`
|
|
|
|
Body any `json:"body"`
|
|
|
|
Proto string `json:"proto"`
|
|
|
|
Host string `json:"host"`
|
|
|
|
Method string `json:"method"`
|
|
|
|
URL string `json:"url"`
|
|
|
|
RemoteAddr string `json:"remoteAddr"`
|
|
|
|
RequestURI string `json:"requestUri"`
|
|
|
|
TransferEncoding []string `json:"transferEncoding"`
|
|
|
|
ContentLength int64 `json:"contentLength"`
|
|
|
|
}
|
|
|
|
|
|
|
|
//nolint:cyclop
|
|
|
|
func readBody(ctx context.Context, r *http.Request) (any, error) {
|
|
|
|
contentType := r.Header.Get("Content-Type")
|
|
|
|
switch {
|
|
|
|
case contentType == "":
|
|
|
|
return nil, nil
|
|
|
|
case strings.HasPrefix(contentType, "application/json"):
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("read: %w", err)
|
|
|
|
log.Error(ctx, err.Error())
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(b, new(any)); err != nil {
|
|
|
|
log.Error(ctx, err.Error())
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return json.RawMessage(b), nil
|
|
|
|
case contentType == "application/x-www-form-urlencoded":
|
|
|
|
fallthrough
|
|
|
|
case strings.HasPrefix(contentType, "application/xml"):
|
|
|
|
fallthrough
|
|
|
|
case strings.HasPrefix(contentType, "text/"):
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return string(b), nil
|
|
|
|
//case :
|
|
|
|
case strings.HasPrefix(contentType, "multipart/form-data"):
|
|
|
|
if err := r.ParseMultipartForm(maxParseMemory); err != nil {
|
|
|
|
log.Error(ctx, err.Error())
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
body := MultipartForm{
|
|
|
|
Values: make(map[string][]string),
|
|
|
|
Files: make(map[string][]MultipartFormFileInfo),
|
|
|
|
}
|
|
|
|
body.Values = r.MultipartForm.Value
|
|
|
|
for k, fs := range r.MultipartForm.File {
|
|
|
|
for _, f := range fs {
|
|
|
|
body.Files[k] = append(body.Files[k], MultipartFormFileInfo{
|
|
|
|
Filename: f.Filename,
|
|
|
|
MIMEHeader: f.Header,
|
|
|
|
Size: f.Size,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return body, nil
|
|
|
|
}
|
|
|
|
b, err := io.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("read: %w", err)
|
|
|
|
log.Error(ctx, err.Error())
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
const maxBodyByteSizeToReturn = 96
|
|
|
|
if len(b) > maxBodyByteSizeToReturn {
|
|
|
|
b = b[:maxBodyByteSizeToReturn]
|
|
|
|
}
|
|
|
|
return b, nil
|
|
|
|
}
|