Moonshark/http/utils.go

189 lines
4.2 KiB
Go

package http
import (
"crypto/rand"
"encoding/base64"
"mime/multipart"
"strings"
"sync"
"github.com/valyala/fasthttp"
)
var emptyMap = make(map[string]any)
var (
stringPool = sync.Pool{
New: func() any {
return make([]string, 0, 4)
},
}
formDataPool = sync.Pool{
New: func() any {
return make(map[string]any, 16)
},
}
)
// QueryToLua converts HTTP query args to a Lua-friendly map
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
args := ctx.QueryArgs()
if args.Len() == 0 {
return emptyMap
}
queryMap := make(map[string]any, args.Len()) // Pre-size
args.VisitAll(func(key, value []byte) {
k := string(key)
v := string(value)
if existing, exists := queryMap[k]; exists {
// Handle multiple values more efficiently
switch typed := existing.(type) {
case []string:
queryMap[k] = append(typed, v)
case string:
// Get slice from pool
slice := stringPool.Get().([]string)
slice = slice[:0] // Reset length
slice = append(slice, typed, v)
queryMap[k] = slice
}
} else {
queryMap[k] = v
}
})
return queryMap
}
// ParseForm extracts form data from a request
func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
contentType := string(ctx.Request.Header.ContentType())
if strings.Contains(contentType, "multipart/form-data") {
return parseMultipartForm(ctx)
}
args := ctx.PostArgs()
if args.Len() == 0 {
return emptyMap, nil
}
formData := formDataPool.Get().(map[string]any)
// Clear the map (should already be clean from pool)
for k := range formData {
delete(formData, k)
}
args.VisitAll(func(key, value []byte) {
k := string(key)
v := string(value)
if existing, exists := formData[k]; exists {
switch typed := existing.(type) {
case []string:
formData[k] = append(typed, v)
case string:
slice := stringPool.Get().([]string)
slice = slice[:0]
slice = append(slice, typed, v)
formData[k] = slice
}
} else {
formData[k] = v
}
})
return formData, nil
}
// parseMultipartForm handles multipart/form-data requests
func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
form, err := ctx.MultipartForm()
if err != nil {
return nil, err
}
formData := formDataPool.Get().(map[string]any)
for k := range formData {
delete(formData, k)
}
// Process form values
for key, values := range form.Value {
switch len(values) {
case 0:
// Skip empty
case 1:
formData[key] = values[0]
default:
formData[key] = values
}
}
// Process files if present
if len(form.File) > 0 {
files := make(map[string]any, len(form.File))
for fieldName, fileHeaders := range form.File {
switch len(fileHeaders) {
case 1:
files[fieldName] = fileInfoToMap(fileHeaders[0])
default:
fileInfos := make([]map[string]any, len(fileHeaders))
for i, fh := range fileHeaders {
fileInfos[i] = fileInfoToMap(fh)
}
files[fieldName] = fileInfos
}
}
formData["_files"] = files
}
return formData, nil
}
// fileInfoToMap converts a FileHeader to a map for Lua
func fileInfoToMap(fh *multipart.FileHeader) map[string]any {
return map[string]any{
"filename": fh.Filename,
"size": fh.Size,
"mimetype": getMimeType(fh),
}
}
// getMimeType gets the mime type from a file header
func getMimeType(fh *multipart.FileHeader) string {
if fh.Header != nil {
contentType := fh.Header.Get("Content-Type")
if contentType != "" {
return contentType
}
}
// Fallback to basic type detection from filename
if strings.HasSuffix(fh.Filename, ".pdf") {
return "application/pdf"
} else if strings.HasSuffix(fh.Filename, ".png") {
return "image/png"
} else if strings.HasSuffix(fh.Filename, ".jpg") || strings.HasSuffix(fh.Filename, ".jpeg") {
return "image/jpeg"
} else if strings.HasSuffix(fh.Filename, ".gif") {
return "image/gif"
} else if strings.HasSuffix(fh.Filename, ".svg") {
return "image/svg+xml"
}
return "application/octet-stream"
}
// GenerateSecureToken creates a cryptographically secure random token
func GenerateSecureToken(length int) (string, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b)[:length], nil
}