245 lines
7.2 KiB
Go
245 lines
7.2 KiB
Go
package errors
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// SecureErrorResponse represents a sanitized error response for clients
|
|
type SecureErrorResponse struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message"`
|
|
RequestID string `json:"request_id,omitempty"`
|
|
Code int `json:"code"`
|
|
}
|
|
|
|
// ErrorHandler provides secure error handling for HTTP responses
|
|
type ErrorHandler struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewErrorHandler creates a new secure error handler
|
|
func NewErrorHandler(logger *zap.Logger) *ErrorHandler {
|
|
return &ErrorHandler{
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// HandleError handles errors securely by logging detailed information and returning sanitized responses
|
|
func (eh *ErrorHandler) HandleError(c *gin.Context, err error, userMessage string) {
|
|
requestID := eh.getOrGenerateRequestID(c)
|
|
|
|
// Log detailed error information for internal debugging
|
|
eh.logger.Error("HTTP request error",
|
|
zap.String("request_id", requestID),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("user_agent", c.Request.UserAgent()),
|
|
zap.String("remote_addr", c.ClientIP()),
|
|
zap.Error(err),
|
|
)
|
|
|
|
// Determine appropriate HTTP status code and error type
|
|
statusCode, errorType := eh.determineErrorResponse(err)
|
|
|
|
// Create sanitized response
|
|
response := SecureErrorResponse{
|
|
Error: errorType,
|
|
Message: eh.sanitizeErrorMessage(userMessage, err),
|
|
RequestID: requestID,
|
|
Code: statusCode,
|
|
}
|
|
|
|
c.JSON(statusCode, response)
|
|
}
|
|
|
|
// HandleValidationError handles input validation errors
|
|
func (eh *ErrorHandler) HandleValidationError(c *gin.Context, field string, message string) {
|
|
requestID := eh.getOrGenerateRequestID(c)
|
|
|
|
eh.logger.Warn("Validation error",
|
|
zap.String("request_id", requestID),
|
|
zap.String("field", field),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
)
|
|
|
|
response := SecureErrorResponse{
|
|
Error: "validation_error",
|
|
Message: "Invalid input provided",
|
|
RequestID: requestID,
|
|
Code: http.StatusBadRequest,
|
|
}
|
|
|
|
c.JSON(http.StatusBadRequest, response)
|
|
}
|
|
|
|
// HandleAuthenticationError handles authentication failures
|
|
func (eh *ErrorHandler) HandleAuthenticationError(c *gin.Context, err error) {
|
|
requestID := eh.getOrGenerateRequestID(c)
|
|
|
|
eh.logger.Warn("Authentication error",
|
|
zap.String("request_id", requestID),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("remote_addr", c.ClientIP()),
|
|
zap.Error(err),
|
|
)
|
|
|
|
response := SecureErrorResponse{
|
|
Error: "authentication_failed",
|
|
Message: "Authentication required",
|
|
RequestID: requestID,
|
|
Code: http.StatusUnauthorized,
|
|
}
|
|
|
|
c.JSON(http.StatusUnauthorized, response)
|
|
}
|
|
|
|
// HandleAuthorizationError handles authorization failures
|
|
func (eh *ErrorHandler) HandleAuthorizationError(c *gin.Context, resource string) {
|
|
requestID := eh.getOrGenerateRequestID(c)
|
|
|
|
eh.logger.Warn("Authorization error",
|
|
zap.String("request_id", requestID),
|
|
zap.String("resource", resource),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("remote_addr", c.ClientIP()),
|
|
)
|
|
|
|
response := SecureErrorResponse{
|
|
Error: "access_denied",
|
|
Message: "Insufficient permissions",
|
|
RequestID: requestID,
|
|
Code: http.StatusForbidden,
|
|
}
|
|
|
|
c.JSON(http.StatusForbidden, response)
|
|
}
|
|
|
|
// HandleInternalError handles internal server errors
|
|
func (eh *ErrorHandler) HandleInternalError(c *gin.Context, err error) {
|
|
requestID := eh.getOrGenerateRequestID(c)
|
|
|
|
eh.logger.Error("Internal server error",
|
|
zap.String("request_id", requestID),
|
|
zap.String("path", c.Request.URL.Path),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("remote_addr", c.ClientIP()),
|
|
zap.Error(err),
|
|
)
|
|
|
|
response := SecureErrorResponse{
|
|
Error: "internal_error",
|
|
Message: "An internal error occurred",
|
|
RequestID: requestID,
|
|
Code: http.StatusInternalServerError,
|
|
}
|
|
|
|
c.JSON(http.StatusInternalServerError, response)
|
|
}
|
|
|
|
// determineErrorResponse determines the appropriate HTTP status and error type
|
|
func (eh *ErrorHandler) determineErrorResponse(err error) (int, string) {
|
|
if appErr, ok := err.(*AppError); ok {
|
|
return appErr.StatusCode, eh.getErrorTypeFromCode(appErr.Code)
|
|
}
|
|
|
|
// For unknown errors, log as internal error but don't expose details
|
|
return http.StatusInternalServerError, "internal_error"
|
|
}
|
|
|
|
// sanitizeErrorMessage removes sensitive information from error messages
|
|
func (eh *ErrorHandler) sanitizeErrorMessage(userMessage string, err error) string {
|
|
if userMessage != "" {
|
|
return userMessage
|
|
}
|
|
|
|
// Provide generic messages for different error types
|
|
if appErr, ok := err.(*AppError); ok {
|
|
return eh.getGenericMessageFromCode(appErr.Code)
|
|
}
|
|
|
|
return "An error occurred"
|
|
}
|
|
|
|
// getErrorTypeFromCode converts an error code to a sanitized error type string
|
|
func (eh *ErrorHandler) getErrorTypeFromCode(code ErrorCode) string {
|
|
switch code {
|
|
case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat:
|
|
return "validation_error"
|
|
case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials:
|
|
return "authentication_failed"
|
|
case ErrForbidden, ErrInsufficientPermissions:
|
|
return "access_denied"
|
|
case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound:
|
|
return "resource_not_found"
|
|
case ErrAlreadyExists, ErrConflict:
|
|
return "resource_conflict"
|
|
case ErrRateLimit:
|
|
return "rate_limit_exceeded"
|
|
case ErrTimeout:
|
|
return "timeout"
|
|
default:
|
|
return "internal_error"
|
|
}
|
|
}
|
|
|
|
// getGenericMessageFromCode provides generic user-safe messages for error codes
|
|
func (eh *ErrorHandler) getGenericMessageFromCode(code ErrorCode) string {
|
|
switch code {
|
|
case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat:
|
|
return "Invalid input provided"
|
|
case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials:
|
|
return "Authentication required"
|
|
case ErrForbidden, ErrInsufficientPermissions:
|
|
return "Access denied"
|
|
case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound:
|
|
return "Resource not found"
|
|
case ErrAlreadyExists, ErrConflict:
|
|
return "Resource conflict"
|
|
case ErrRateLimit:
|
|
return "Rate limit exceeded"
|
|
case ErrTimeout:
|
|
return "Request timeout"
|
|
default:
|
|
return "An error occurred"
|
|
}
|
|
}
|
|
|
|
// getOrGenerateRequestID gets or generates a request ID for tracking
|
|
func (eh *ErrorHandler) getOrGenerateRequestID(c *gin.Context) string {
|
|
// Try to get existing request ID from context
|
|
if requestID, exists := c.Get("request_id"); exists {
|
|
if id, ok := requestID.(string); ok {
|
|
return id
|
|
}
|
|
}
|
|
|
|
// Try to get from header
|
|
requestID := c.GetHeader("X-Request-ID")
|
|
if requestID != "" {
|
|
return requestID
|
|
}
|
|
|
|
// Generate a simple request ID (in production, use a proper UUID library)
|
|
return generateSimpleID()
|
|
}
|
|
|
|
// generateSimpleID generates a simple request ID
|
|
func generateSimpleID() string {
|
|
// Simple implementation - in production use proper UUID generation
|
|
bytes := make([]byte, 8)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
// Fallback to timestamp-based ID
|
|
return fmt.Sprintf("req_%d", time.Now().UnixNano())
|
|
}
|
|
return "req_" + hex.EncodeToString(bytes)
|
|
} |