Files
2026-03-30 22:05:57 +08:00

215 lines
4.9 KiB
Go

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(`<!DOCTYPE html>
<html>
<head>
<title>%s</title>
<meta charset="utf-8">
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
margin: 0;
padding: 50px;
}
.error-container {
background-color: white;
border-radius: 10px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 500px;
margin: 0 auto;
}
h1 {
color: #e74c3c;
font-size: 72px;
margin: 0 0 20px 0;
}
p {
color: #333;
font-size: 18px;
margin: 0 0 20px 0;
}
.back-link {
color: #3498db;
text-decoration: none;
}
.back-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="error-container">
<h1>%s</h1>
<p>%s</p>
<a href="/" class="back-link">返回首页</a>
</div>
</body>
</html>`, title, statusNum, message)
return html
}
func SendResponse(conn io.Writer, response *Response) error {
data := response.Build()
_, err := conn.Write(data)
return err
}