You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crate/cmd/http-reflect-server/handler.go

161 lines
4.0 KiB
Go

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
}