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

559 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"computer-network/logger"
"encoding/json"
"fmt"
"html"
"net"
"strings"
"sync"
)
type AdminHandler struct {
mu sync.RWMutex
}
func NewAdminHandler() *AdminHandler {
return &AdminHandler{}
}
func (h *AdminHandler) ClearLogs() *HTTPResponse {
h.mu.Lock()
defer h.mu.Unlock()
logger.ClearLogs()
jsonData, _ := json.Marshal(map[string]interface{}{
"success": true,
"message": "Logs cleared successfully",
})
return &HTTPResponse{
StatusCode: "200 OK",
ContentType: "application/json",
Body: string(jsonData),
}
}
func (h *AdminHandler) GetIndexPage() *HTTPResponse {
html := `<!DOCTYPE html>
<html>
<head>
<title>Web Server Admin Console</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
}
.header h1 {
font-size: 28px;
margin-bottom: 5px;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.content {
padding: 30px;
}
.status-card {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.status-item {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.status-item h3 {
color: #666;
font-size: 12px;
text-transform: uppercase;
margin-bottom: 10px;
}
.status-item .value {
color: #333;
font-size: 24px;
font-weight: bold;
}
.status-item .value.running {
color: #27ae60;
}
.status-item .value.stopped {
color: #e74c3c;
}
.controls {
display: flex;
gap: 15px;
margin-bottom: 30px;
}
button {
flex: 1;
padding: 12px 24px;
font-size: 14px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
button:active {
transform: translateY(0);
}
.btn-status {
background: #27ae60;
color: white;
}
.btn-logs {
background: #3498db;
color: white;
}
.btn-clear {
background: #e74c3c;
color: white;
}
.btn-refresh {
background: #f39c12;
color: white;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.logs-section {
background: #000;
border-radius: 8px;
padding: 0;
overflow: hidden;
}
.logs-section h2 {
color: #333;
text-transform: uppercase;
font-size: 18px;
margin-bottom: 15px;
}
.log-container {
background: white;
border-radius: 8px;
padding: 20px;
max-height: 400px;
overflow-y: auto;
}
.log-entry {
padding: 8px 0;
border-bottom: 1px solid #eee;
font-family: 'Courier New', monospace;
font-size: 12px;
color: #333;
}
.log-entry:last-child {
border-bottom: none;
}
.log-entry .timestamp {
color: #667eea;
font-weight: bold;
margin-right: 8px;
}
.log-entry .ip {
color: #764ba2;
margin-right: 8px;
}
.log-entry .method {
font-weight: bold;
margin-right: 5px;
}
.log-entry .url {
color: #333;
margin-right: 5px;
}
.log-entry .status {
margin-right: 5px;
}
.log-entry .status-200 {
color: #27ae60;
}
.log-entry .status-404 {
color: #e74c3c;
}
.log-entry .status-501 {
color: #f39c12;
}
.log-entry .duration {
color: #999;
}
.empty-logs {
text-align: center;
color: #999;
padding: 40px;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Web服务器管理控制台</h1>
<p>管理和监控您的Web服务器</p>
</div>
<div class="content">
<div class="status-card">
<div class="status-item">
<h3>服务器状态</h3>
<div class="value running" id="server-status">加载中...</div>
</div>
<div class="status-item">
<h3>监听端口</h3>
<div class="value" id="server-port">加载中...</div>
</div>
<div class="status-item">
<h3>总请求数</h3>
<div class="value" id="total-requests">0</div>
</div>
<div class="status-item">
<h3>管理端口</h3>
<div class="value">9001</div>
</div>
</div>
<div class="controls">
<button class="btn-status" id="status-toggle" onclick="refreshStatus()">刷新状态</button>
<button class="btn-logs" onclick="window.location.href='/logs'">查看全部日志</button>
<button class="btn-clear" onclick="clearLogs()">清空日志</button>
<button class="btn-refresh" onclick="refreshLogs()">刷新日志</button>
</div>
<div class="logs-section">
<h2>最近访问日志</h2>
<div class="log-container">
<div class="empty-logs">加载日志中...</div>
</div>
</div>
</div>
</div>
<script>
function refreshLogs() {
fetch('/api/logs')
.then(response => response.json())
.then(data => {
const container = document.querySelector('.log-container');
if (data.logs && data.logs.length > 0) {
container.innerHTML = data.logs.map(log => {
const date = new Date(log.timestamp);
const timestamp = date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const duration = log.duration / 1000000;
return '<div class="log-entry">' +
'<span class="timestamp">[' + timestamp + ']</span>' +
'<span class="ip">' + log.client_ip + '</span>' +
'<span class="method">' + log.method + '</span>' +
'<span class="url">' + log.url + '</span>' +
'<span class="status status-' + log.status_code + '">' + log.status_code + '</span>' +
'<span class="duration">' + duration.toFixed(2) + 'ms</span>' +
'</div>';
}).join('');
document.getElementById('total-requests').textContent = data.logs.length;
const logs = container.querySelectorAll('.log-entry');
if (logs.length > 10) {
for (let i = 0; i < logs.length - 10; i++) {
logs[i].style.display = 'none';
}
}
} else {
container.innerHTML = '<div class="empty-logs">暂无访问日志</div>';
}
})
.catch(error => {
console.error('Error fetching logs:', error);
document.querySelector('.log-container').innerHTML = '<div class="empty-logs">加载日志失败</div>';
});
}
function clearLogs() {
fetch('/api/logs', {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
refreshLogs();
alert('日志清空成功!');
} else {
alert('清空日志失败:' + (data.error || '未知错误'));
}
})
.catch(error => {
console.error('Error clearing logs:', error);
alert('清空日志时出错');
});
}
function refreshStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const statusEl = document.getElementById('server-status');
statusEl.textContent = data.running ? '运行中' : '已停止';
statusEl.className = 'value ' + (data.running ? 'running' : 'stopped');
document.getElementById('server-port').textContent = data.port;
})
.catch(error => {
console.error('Error fetching status:', error);
document.getElementById('server-status').textContent = '未知';
document.getElementById('server-status').className = 'value stopped';
});
}
refreshLogs();
refreshStatus();
setInterval(refreshLogs, 5000);
setInterval(refreshStatus, 5000);
</script>
</body>
</html>`
return &HTTPResponse{
StatusCode: "200 OK",
ContentType: "text/html; charset=utf-8",
Body: html,
}
}
func (h *AdminHandler) GetLogsPage() *HTTPResponse {
logs := logger.GetLogs()
var logEntries []string
if len(logs) == 0 {
logEntries = append(logEntries, `<div class="empty-logs">暂无访问日志</div>`)
} else {
for i := len(logs) - 1; i >= 0; i-- {
log := logs[i]
date := log.Timestamp.Format("2006-01-02 15:04:05")
duration := log.Duration.Milliseconds()
statusClass := fmt.Sprintf("status-%d", log.StatusCode)
logEntry := fmt.Sprintf(`<div class="log-entry">
<span class="timestamp">[%s]</span>
<span class="ip">%s</span>
<span class="method">%s</span>
<span class="url">%s</span>
<span class="status %s">%d</span>
<span class="duration">%dms</span>
</div>`,
date, log.ClientIP, log.Method, html.EscapeString(log.URL), statusClass, log.StatusCode, duration)
logEntries = append(logEntries, logEntry)
}
}
html := fmt.Sprintf(`<!DOCTYPE html>
<html>
<head>
<title>访问日志 - Web服务器管理</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
color: white;
padding: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 { font-size: 24px; }
.back-btn {
background: white;
color: #667eea;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
text-decoration: none;
}
.content { padding: 30px; }
.log-container {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
max-height: 600px;
overflow-y: auto;
}
.log-entry {
padding: 10px 0;
border-bottom: 1px solid #dee2e6;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #333;
}
.log-entry:last-child { border-bottom: none; }
.log-entry .timestamp { color: #667eea; font-weight: bold; margin-right: 10px; }
.log-entry .ip { color: #764ba2; margin-right: 10px; }
.log-entry .method { font-weight: bold; margin-right: 5px; }
.log-entry .url { color: #333; margin-right: 10px; }
.log-entry .status { margin-right: 10px; font-weight: bold; }
.log-entry .status-200 { color: #27ae60; }
.log-entry .status-404 { color: #e74c3c; }
.log-entry .status-501 { color: #f39c12; }
.log-entry .duration { color: #999; }
.empty-logs {
text-align: center;
color: #999;
padding: 40px;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>访问日志(总计:%d)</h1>
<a href="/" class="back-btn">← 返回管理面板</a>
</div>
<div class="content">
<div class="log-container">%s</div>
</div>
</div>
</body>
</html>`, len(logs), strings.Join(logEntries, ""))
return &HTTPResponse{
StatusCode: "200 OK",
ContentType: "text/html; charset=utf-8",
Body: html,
}
}
type LogEntryJSON struct {
ClientIP string `json:"client_ip"`
Method string `json:"method"`
URL string `json:"url"`
HTTPVersion string `json:"http_version"`
StatusCode int `json:"status_code"`
Duration int64 `json:"duration"`
Timestamp string `json:"timestamp"`
}
func (h *AdminHandler) GetLogsJSON() *HTTPResponse {
h.mu.RLock()
defer h.mu.RUnlock()
logs := logger.GetLogs()
jsonLogs := make([]LogEntryJSON, len(logs))
for i, log := range logs {
jsonLogs[i] = LogEntryJSON{
ClientIP: log.ClientIP,
Method: log.Method,
URL: log.URL,
HTTPVersion: log.HTTPVersion,
StatusCode: log.StatusCode,
Duration: log.Duration.Nanoseconds(),
Timestamp: log.Timestamp.Format("2006-01-02T15:04:05Z07:00"),
}
}
jsonData, _ := json.Marshal(map[string]interface{}{
"logs": jsonLogs,
"count": len(logs),
})
return &HTTPResponse{
StatusCode: "200 OK",
ContentType: "application/json",
Body: string(jsonData),
}
}
func (h *AdminHandler) GetStatusJSON() *HTTPResponse {
h.mu.RLock()
defer h.mu.RUnlock()
jsonData, _ := json.Marshal(map[string]interface{}{
"running": true,
"port": 8080,
})
return &HTTPResponse{
StatusCode: "200 OK",
ContentType: "application/json",
Body: string(jsonData),
}
}
type HTTPResponse struct {
StatusCode string
ContentType string
Body string
}
func (r *HTTPResponse) 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)))
builder.WriteString("\r\n")
builder.WriteString(r.Body)
return []byte(builder.String())
}
func SendResponse(conn net.Conn, response *HTTPResponse) error {
data := response.Build()
_, err := conn.Write(data)
return err
}