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 }