diff --git a/cmd/http-reflect-server/go.mod b/cmd/http-reflect-server/go.mod new file mode 100644 index 0000000..4913589 --- /dev/null +++ b/cmd/http-reflect-server/go.mod @@ -0,0 +1,13 @@ +module git.blauwelle.com/go/crate/cmd/http-reflect-server + +go 1.20 + +require ( + git.blauwelle.com/go/crate/exegroup v0.3.0 + git.blauwelle.com/go/crate/log v0.4.0 +) + +require ( + git.blauwelle.com/go/crate/runtimehelper v0.1.0 // indirect + git.blauwelle.com/go/crate/synchelper v0.1.0 // indirect +) diff --git a/cmd/http-reflect-server/go.sum b/cmd/http-reflect-server/go.sum new file mode 100644 index 0000000..c04048f --- /dev/null +++ b/cmd/http-reflect-server/go.sum @@ -0,0 +1,8 @@ +git.blauwelle.com/go/crate/exegroup v0.3.0 h1:TBLygDztECKc67NeIIBsFDxlA4KcJpbOmafqqRuKRcM= +git.blauwelle.com/go/crate/exegroup v0.3.0/go.mod h1:DJoID54YI5WFHGHoTCjBao8oS3HFRzwbWMZW6P57AIQ= +git.blauwelle.com/go/crate/log v0.4.0 h1:wK/qwO+a2YE51F6LdC9pZXL2AIARCRW0+AvFIF2Txt8= +git.blauwelle.com/go/crate/log v0.4.0/go.mod h1:NfiG7YKQCTnLIcn6fVkaa2qEu+DuYi1Kz783Sc/F3jI= +git.blauwelle.com/go/crate/runtimehelper v0.1.0 h1:qNhtnt9YmHXNHKsGRbwD3AZ3pezpOwrbmX1o9Bz532I= +git.blauwelle.com/go/crate/runtimehelper v0.1.0/go.mod h1:yVMA0GkO9AS7iuPmalHKeWyv9en0JWj25rY1vpTuHhk= +git.blauwelle.com/go/crate/synchelper v0.1.0 h1:4yEXpshkklaws/57P94xN5bA3NmyyKGcZqYmzd6QIK4= +git.blauwelle.com/go/crate/synchelper v0.1.0/go.mod h1:2JkfH+7sF0Q0wiIaDOqG42ZLO5JxpcMfSoyy7db4Y2g= diff --git a/cmd/http-reflect-server/handler.go b/cmd/http-reflect-server/handler.go new file mode 100644 index 0000000..093ec8d --- /dev/null +++ b/cmd/http-reflect-server/handler.go @@ -0,0 +1,155 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "git.blauwelle.com/go/crate/log" +) + +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"` +} + +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(16 * 1024 * 1024); 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 + } + if len(b) > 96 { + b = b[:96] + } + return b, nil +} diff --git a/cmd/http-reflect-server/main.go b/cmd/http-reflect-server/main.go new file mode 100644 index 0000000..3c541b4 --- /dev/null +++ b/cmd/http-reflect-server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "context" + "flag" + "net/http" + + "git.blauwelle.com/go/crate/exegroup" + "git.blauwelle.com/go/crate/log" + "git.blauwelle.com/go/crate/log/logsdk" + "git.blauwelle.com/go/crate/log/logsdk/logjson" +) + +var port = flag.Int("port", 8080, "HTTP Port") + +func main() { + flag.Parse() + log.Logger().AddProcessor(logsdk.AllLevels, logjson.New()) + g := exegroup.Default() + mux := http.NewServeMux() + var handler http.Handler = mux + mux.HandleFunc("/", newHandler()) + g.New().WithGoStop(exegroup.HttpListenAndServe(*port, handler)) + log.Infof(context.Background(), "listening %d", *port) + log.Error(context.Background(), "exit: ", g.Run(context.Background())) +}