215 lines
4.9 KiB
Go
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
|
|
}
|