207 lines
5.3 KiB
Go
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
|
|
}
|