From 3d61501eb9a7b6e5b6fd444cb91af602a0e265ff Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 6 Mar 2025 20:07:32 -0600 Subject: [PATCH] forms --- core/http/forms.go | 136 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 core/http/forms.go diff --git a/core/http/forms.go b/core/http/forms.go new file mode 100644 index 0000000..b68d4e6 --- /dev/null +++ b/core/http/forms.go @@ -0,0 +1,136 @@ +package http + +import ( + "errors" + "io" + "mime" + "net/http" + "net/url" + "strings" +) + +// Maximum form parse size (16MB) +const maxFormSize = 16 << 20 + +// Common errors +var ( + ErrFormSizeTooLarge = errors.New("form size too large") + ErrInvalidFormType = errors.New("invalid form content type") +) + +// ParseForm parses a POST request body into a map of values +// Supports both application/x-www-form-urlencoded and multipart/form-data content types +func ParseForm(r *http.Request) (map[string]any, error) { + // Only handle POST, PUT, PATCH + if r.Method != http.MethodPost && + r.Method != http.MethodPut && + r.Method != http.MethodPatch { + return make(map[string]any), nil + } + + // Check content type + contentType := r.Header.Get("Content-Type") + if contentType == "" { + return make(map[string]any), nil + } + + // Parse the media type + mediaType, params, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, ErrInvalidFormType + } + + result := make(map[string]any) + + switch { + case mediaType == "application/x-www-form-urlencoded": + // Handle URL-encoded form + if err := parseURLEncodedForm(r, result); err != nil { + return nil, err + } + + case strings.HasPrefix(mediaType, "multipart/form-data"): + // Handle multipart form + boundary := params["boundary"] + if boundary == "" { + return nil, ErrInvalidFormType + } + + if err := parseMultipartForm(r, boundary, result); err != nil { + return nil, err + } + + default: + // Unrecognized content type + return make(map[string]any), nil + } + + return result, nil +} + +// parseURLEncodedForm handles application/x-www-form-urlencoded forms +func parseURLEncodedForm(r *http.Request, result map[string]any) error { + // Enforce size limit + r.Body = http.MaxBytesReader(nil, r.Body, maxFormSize) + + // Read the entire body + body, err := io.ReadAll(r.Body) + if err != nil { + if strings.Contains(err.Error(), "http: request body too large") { + return ErrFormSizeTooLarge + } + return err + } + + // Parse form values + form, err := url.ParseQuery(string(body)) + if err != nil { + return err + } + + // Convert to map[string]any + for key, values := range form { + if len(values) == 1 { + // Single value + result[key] = values[0] + } else if len(values) > 1 { + // Multiple values + result[key] = values + } + } + + return nil +} + +// parseMultipartForm handles multipart/form-data forms +func parseMultipartForm(r *http.Request, boundary string, result map[string]any) error { + // Limit the form size + if err := r.ParseMultipartForm(maxFormSize); err != nil { + if strings.Contains(err.Error(), "http: request body too large") { + return ErrFormSizeTooLarge + } + return err + } + + // Process form values + for key, values := range r.MultipartForm.Value { + if len(values) == 1 { + // Single value + result[key] = values[0] + } else if len(values) > 1 { + // Multiple values + result[key] = values + } + } + + // We don't handle file uploads here - could be extended in the future + // if needed to support file uploads to Lua + + return nil +} + +// Usage: +// After parsing the form with ParseForm, you can add it to the context with: +// ctx.Set("form", formData) +// +// This makes the form data accessible in Lua as ctx.form.field_name