Moonshark/core/http/Utils.go
2025-04-09 19:03:35 -05:00

207 lines
5.3 KiB
Go

package http
import (
"crypto/rand"
"encoding/base64"
"fmt"
"mime/multipart"
"strings"
"time"
"Moonshark/core/utils/logger"
"github.com/valyala/fasthttp"
)
// LogRequest logs an HTTP request with its status code and duration
func LogRequest(statusCode int, method, path string, duration time.Duration) {
var statusColor, resetColor, methodColor string
// Status code colors
if statusCode >= 200 && statusCode < 300 {
statusColor = "\u001b[32m" // Green for 2xx
} else if statusCode >= 300 && statusCode < 400 {
statusColor = "\u001b[36m" // Cyan for 3xx
} else if statusCode >= 400 && statusCode < 500 {
statusColor = "\u001b[33m" // Yellow for 4xx
} else {
statusColor = "\u001b[31m" // Red for 5xx and others
}
// Method colors
switch method {
case "GET":
methodColor = "\u001b[32m" // Green
case "POST":
methodColor = "\u001b[34m" // Blue
case "PUT":
methodColor = "\u001b[33m" // Yellow
case "DELETE":
methodColor = "\u001b[31m" // Red
default:
methodColor = "\u001b[35m" // Magenta for others
}
resetColor = "\u001b[0m"
// Format duration
var durationStr string
if duration.Milliseconds() < 1 {
durationStr = fmt.Sprintf("%.2fµs", float64(duration.Microseconds()))
} else if duration.Milliseconds() < 1000 {
durationStr = fmt.Sprintf("%.2fms", float64(duration.Microseconds())/1000)
} else {
durationStr = fmt.Sprintf("%.2fs", duration.Seconds())
}
// Log with colors
logger.Server("%s%d%s %s%s%s %s %s",
statusColor, statusCode, resetColor,
methodColor, method, resetColor,
path, durationStr)
}
// QueryToLua converts HTTP query args to a Lua-friendly map
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
queryMap := make(map[string]any)
// Visit all query parameters
ctx.QueryArgs().VisitAll(func(key, value []byte) {
// Convert to string
k := string(key)
v := string(value)
// Check if this key already exists as an array
if existing, ok := queryMap[k]; ok {
// If it's already an array, append to it
if arr, ok := existing.([]string); ok {
queryMap[k] = append(arr, v)
} else if str, ok := existing.(string); ok {
// Convert existing string to array and append new value
queryMap[k] = []string{str, v}
}
} else {
// New key, store as string
queryMap[k] = v
}
})
return queryMap
}
// ParseForm extracts form data from a request
func ParseForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
formData := make(map[string]any)
// Check if multipart form
if strings.Contains(string(ctx.Request.Header.ContentType()), "multipart/form-data") {
return parseMultipartForm(ctx)
}
// Regular form
ctx.PostArgs().VisitAll(func(key, value []byte) {
k := string(key)
v := string(value)
// Check if this key already exists
if existing, ok := formData[k]; ok {
// If it's already an array, append to it
if arr, ok := existing.([]string); ok {
formData[k] = append(arr, v)
} else if str, ok := existing.(string); ok {
// Convert existing string to array and append new value
formData[k] = []string{str, v}
}
} else {
// New key, store as string
formData[k] = v
}
})
return formData, nil
}
// parseMultipartForm handles multipart/form-data requests
func parseMultipartForm(ctx *fasthttp.RequestCtx) (map[string]any, error) {
formData := make(map[string]any)
// Parse multipart form
form, err := ctx.MultipartForm()
if err != nil {
return nil, err
}
// Process form values
for key, values := range form.Value {
if len(values) == 1 {
formData[key] = values[0]
} else if len(values) > 1 {
formData[key] = values
}
}
// Process files (store file info, not the content)
if len(form.File) > 0 {
files := make(map[string]any)
for fieldName, fileHeaders := range form.File {
if len(fileHeaders) == 1 {
files[fieldName] = fileInfoToMap(fileHeaders[0])
} else if len(fileHeaders) > 1 {
fileInfos := make([]map[string]any, 0, len(fileHeaders))
for _, fh := range fileHeaders {
fileInfos = append(fileInfos, 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
}