package server import ( "fmt" "io" "os" "path/filepath" "strings" ) const ( StatusOK = "200 OK" StatusNotFound = "404 Not Found" StatusNotImplemented = "501 Not Implemented" StatusInternalServerError = "500 Internal Server Error" StatusBadRequest = "400 Bad Request" ) type Response struct { StatusCode string Headers map[string]string Body string ContentType string } func getContentType(filePath string) string { ext := strings.ToLower(filepath.Ext(filePath)) switch ext { case ".html": return "text/html; charset=utf-8" case ".htm": return "text/html; charset=utf-8" case ".css": return "text/css; charset=utf-8" case ".js": return "application/javascript" case ".json": return "application/json" case ".jpg": return "image/jpeg" case ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".gif": return "image/gif" case ".svg": return "image/svg+xml" case ".ico": return "image/x-icon" case ".txt": return "text/plain; charset=utf-8" case ".pdf": return "application/pdf" default: return "application/octet-stream" } } func NewResponse(statusCode, body, contentType string) *Response { return &Response{ StatusCode: statusCode, Body: body, ContentType: contentType, Headers: make(map[string]string), } } func (r *Response) Build() []byte { var builder strings.Builder builder.WriteString(fmt.Sprintf("HTTP/1.1 %s\r\n", r.StatusCode)) builder.WriteString(fmt.Sprintf("Content-Type: %s\r\n", r.ContentType)) builder.WriteString(fmt.Sprintf("Content-Length: %d\r\n", len(r.Body))) for key, value := range r.Headers { builder.WriteString(fmt.Sprintf("%s: %s\r\n", key, value)) } builder.WriteString("\r\n") builder.WriteString(r.Body) return []byte(builder.String()) } func ReadFile(rootDir, path string) ([]byte, string, error) { filePath := filepath.Join(rootDir, path) filePath = filepath.Clean(filePath) if strings.Contains(filePath, "..") { return nil, "", fmt.Errorf("invalid path") } data, err := os.ReadFile(filePath) if err != nil { return nil, "", err } contentType := getContentType(filePath) return data, contentType, nil } func BuildOKResponse(body []byte, contentType string) *Response { return &Response{ StatusCode: StatusOK, Body: string(body), ContentType: contentType, Headers: make(map[string]string), } } func BuildErrorResponse(statusCode, htmlPath, rootDir string) *Response { body := getErrorHTML(statusCode) if htmlPath != "" && rootDir != "" { data, contentType, err := ReadFile(rootDir, htmlPath) if err == nil { return &Response{ StatusCode: statusCode, Body: string(data), ContentType: contentType, Headers: make(map[string]string), } } } return &Response{ StatusCode: statusCode, Body: body, ContentType: "text/html; charset=utf-8", Headers: make(map[string]string), } } func getErrorHTML(statusCode string) string { statusNum := strings.Split(statusCode, " ")[0] var title, message string switch statusCode { case StatusNotFound: title = "404 Not Found" message = "The requested resource could not be found." case StatusNotImplemented: title = "501 Not Implemented" message = "The requested method is not supported by this server. Only GET method is supported." case StatusInternalServerError: title = "500 Internal Server Error" message = "An internal server error occurred." case StatusBadRequest: title = "400 Bad Request" message = "The request could not be understood by the server." default: title = statusCode message = "An error occurred while processing your request." } html := fmt.Sprintf(`