first commit

This commit is contained in:
MiaoMint
2025-09-23 01:47:48 +08:00
commit 3ce69dbe2d
10 changed files with 1197 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
package scanner
import (
"bufio"
"fmt"
"net"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
"github.com/miaomint/port-manager/pkg/types"
)
// PortScanner 端口扫描器
type PortScanner struct {
includeListening bool
includeEstablished bool
}
// NewPortScanner 创建新的端口扫描器
func NewPortScanner(includeListening, includeEstablished bool) *PortScanner {
return &PortScanner{
includeListening: includeListening,
includeEstablished: includeEstablished,
}
}
// ScanPorts 扫描本地端口
func (ps *PortScanner) ScanPorts() (*types.ScanResult, error) {
var ports []types.PortInfo
// 获取TCP端口
tcpPorts, err := ps.scanTCPPorts()
if err != nil {
return nil, fmt.Errorf("扫描TCP端口失败: %v", err)
}
ports = append(ports, tcpPorts...)
// 获取UDP端口
udpPorts, err := ps.scanUDPPorts()
if err != nil {
return nil, fmt.Errorf("扫描UDP端口失败: %v", err)
}
ports = append(ports, udpPorts...)
return &types.ScanResult{
Ports: ports,
Timestamp: time.Now(),
TotalPorts: len(ports),
}, nil
}
// scanTCPPorts 扫描TCP端口使用netstat
func (ps *PortScanner) scanTCPPorts() ([]types.PortInfo, error) {
var args []string
args = append(args, "-an")
args = append(args, "-p", "tcp")
cmd := exec.Command("netstat", args...)
output, err := cmd.Output()
if err != nil {
return nil, err
}
return ps.parseNetstatOutput(string(output), "tcp")
}
// scanUDPPorts 扫描UDP端口
func (ps *PortScanner) scanUDPPorts() ([]types.PortInfo, error) {
var args []string
args = append(args, "-an")
args = append(args, "-p", "udp")
cmd := exec.Command("netstat", args...)
output, err := cmd.Output()
if err != nil {
return nil, err
}
return ps.parseNetstatOutput(string(output), "udp")
}
// parseNetstatOutput 解析netstat输出
func (ps *PortScanner) parseNetstatOutput(output, protocol string) ([]types.PortInfo, error) {
var ports []types.PortInfo
scanner := bufio.NewScanner(strings.NewReader(output))
// 跳过标题行
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "Local Address") {
break
}
}
// 解析每一行
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
port, err := ps.parseNetstatLine(line, protocol)
if err != nil {
continue // 跳过解析失败的行
}
if port != nil {
// 过滤状态
if ps.shouldIncludePort(port) {
ports = append(ports, *port)
}
}
}
return ports, nil
}
// parseNetstatLine 解析netstat的单行输出
func (ps *PortScanner) parseNetstatLine(line, protocol string) (*types.PortInfo, error) {
fields := regexp.MustCompile(`\s+`).Split(line, -1)
if len(fields) < 3 {
return nil, fmt.Errorf("invalid line format")
}
// 解析本地地址
localAddr := fields[0]
parts := strings.Split(localAddr, ":")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid address format")
}
portStr := parts[len(parts)-1]
port, err := strconv.Atoi(portStr)
if err != nil {
return nil, fmt.Errorf("invalid port: %s", portStr)
}
state := ""
if protocol == "tcp" && len(fields) >= 4 {
state = fields[3]
}
return &types.PortInfo{
Port: port,
Protocol: protocol,
State: state,
LastSeen: time.Now(),
}, nil
}
// shouldIncludePort 判断是否应该包含此端口
func (ps *PortScanner) shouldIncludePort(port *types.PortInfo) bool {
if port.Protocol == "tcp" {
if port.State == "LISTEN" {
return ps.includeListening
}
if port.State == "ESTABLISHED" {
return ps.includeEstablished
}
}
// UDP端口默认包含
if port.Protocol == "udp" {
return ps.includeListening
}
return false
}
// ScanPortRange 扫描指定端口范围
func (ps *PortScanner) ScanPortRange(start, end int) ([]int, error) {
var openPorts []int
for port := start; port <= end; port++ {
// TCP扫描
conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", port), time.Millisecond*100)
if err == nil {
conn.Close()
openPorts = append(openPorts, port)
}
}
return openPorts, nil
}
// IsPortOpen 检查指定端口是否开放
func (ps *PortScanner) IsPortOpen(port int, protocol string) bool {
address := fmt.Sprintf("localhost:%d", port)
conn, err := net.DialTimeout(protocol, address, time.Millisecond*100)
if err != nil {
return false
}
conn.Close()
return true
}
// GenerateRandomPort 生成一个未被占用的随机端口
func (ps *PortScanner) GenerateRandomPort(minPort, maxPort int) (int, error) {
if minPort <= 0 {
minPort = 8000
}
if maxPort <= 0 || maxPort > 65535 {
maxPort = 65535
}
for attempts := 0; attempts < 100; attempts++ {
port := minPort + (int(time.Now().UnixNano()) % (maxPort - minPort + 1))
if !ps.IsPortOpen(port, "tcp") {
return port, nil
}
}
return 0, fmt.Errorf("无法找到可用端口")
}

View File

@@ -0,0 +1,180 @@
package service
import (
"fmt"
"net/http"
"time"
"github.com/miaomint/port-manager/pkg/types"
)
// ServiceIdentifier 服务识别器
type ServiceIdentifier struct {
wellKnownPorts map[int]types.ServiceMapping
}
// NewServiceIdentifier 创建新的服务识别器
func NewServiceIdentifier() *ServiceIdentifier {
return &ServiceIdentifier{
wellKnownPorts: initWellKnownPorts(),
}
}
// IdentifyService 识别端口对应的服务
func (si *ServiceIdentifier) IdentifyService(port *types.PortInfo) *types.PortInfo {
// 复制端口信息
result := *port
// 查找已知服务
if mapping, exists := si.wellKnownPorts[port.Port]; exists {
result.ServiceName = mapping.ServiceName
// 如果是HTTP/HTTPS服务尝试构建访问URL
if mapping.IsHTTP || mapping.IsHTTPS {
protocol := "http"
if mapping.IsHTTPS {
protocol = "https"
}
result.ServiceURL = fmt.Sprintf("%s://localhost:%d", protocol, port.Port)
// 验证HTTP服务是否可访问
if si.isHTTPServiceActive(result.ServiceURL) {
result.ServiceName = fmt.Sprintf("%s (可访问)", result.ServiceName)
}
}
} else {
// 尝试探测HTTP服务
if si.probeHTTPService(port.Port) {
result.ServiceName = "HTTP服务"
result.ServiceURL = fmt.Sprintf("http://localhost:%d", port.Port)
} else if si.probeHTTPSService(port.Port) {
result.ServiceName = "HTTPS服务"
result.ServiceURL = fmt.Sprintf("https://localhost:%d", port.Port)
} else {
result.ServiceName = "未知服务"
}
}
return &result
}
// probeHTTPService 探测HTTP服务
func (si *ServiceIdentifier) probeHTTPService(port int) bool {
url := fmt.Sprintf("http://localhost:%d", port)
return si.isHTTPServiceActive(url)
}
// probeHTTPSService 探测HTTPS服务
func (si *ServiceIdentifier) probeHTTPSService(port int) bool {
url := fmt.Sprintf("https://localhost:%d", port)
return si.isHTTPServiceActive(url)
}
// isHTTPServiceActive 检查HTTP服务是否活跃
func (si *ServiceIdentifier) isHTTPServiceActive(url string) bool {
client := &http.Client{
Timeout: 2 * time.Second,
}
resp, err := client.Get(url)
if err != nil {
return false
}
defer resp.Body.Close()
// 任何HTTP响应都认为是活跃的HTTP服务
return true
}
// initWellKnownPorts 初始化已知端口映射
func initWellKnownPorts() map[int]types.ServiceMapping {
return map[int]types.ServiceMapping{
// Web服务
80: {Port: 80, ServiceName: "HTTP", Description: "超文本传输协议", IsHTTP: true},
443: {Port: 443, ServiceName: "HTTPS", Description: "安全超文本传输协议", IsHTTPS: true},
8080: {Port: 8080, ServiceName: "HTTP代理", Description: "HTTP代理服务", IsHTTP: true},
8443: {Port: 8443, ServiceName: "HTTPS代理", Description: "HTTPS代理服务", IsHTTPS: true},
3000: {Port: 3000, ServiceName: "开发服务器", Description: "React/Node.js开发服务器", IsHTTP: true},
3001: {Port: 3001, ServiceName: "开发服务器", Description: "开发服务器", IsHTTP: true},
4000: {Port: 4000, ServiceName: "开发服务器", Description: "Jekyll/Hugo开发服务器", IsHTTP: true},
8000: {Port: 8000, ServiceName: "HTTP服务", Description: "Python HTTP服务器", IsHTTP: true},
9000: {Port: 9000, ServiceName: "PHP-FPM", Description: "PHP FastCGI Process Manager", IsHTTP: true},
// 数据库
3306: {Port: 3306, ServiceName: "MySQL", Description: "MySQL数据库服务器"},
5432: {Port: 5432, ServiceName: "PostgreSQL", Description: "PostgreSQL数据库服务器"},
27017: {Port: 27017, ServiceName: "MongoDB", Description: "MongoDB数据库服务器"},
6379: {Port: 6379, ServiceName: "Redis", Description: "Redis内存数据库"},
11211: {Port: 11211, ServiceName: "Memcached", Description: "Memcached缓存服务器"},
// 远程访问
22: {Port: 22, ServiceName: "SSH", Description: "安全外壳协议"},
23: {Port: 23, ServiceName: "Telnet", Description: "远程登录协议"},
3389: {Port: 3389, ServiceName: "RDP", Description: "远程桌面协议"},
5900: {Port: 5900, ServiceName: "VNC", Description: "虚拟网络计算"},
// 邮件服务
25: {Port: 25, ServiceName: "SMTP", Description: "简单邮件传输协议"},
110: {Port: 110, ServiceName: "POP3", Description: "邮局协议版本3"},
143: {Port: 143, ServiceName: "IMAP", Description: "互联网消息访问协议"},
465: {Port: 465, ServiceName: "SMTPS", Description: "安全SMTP"},
587: {Port: 587, ServiceName: "SMTP-Submission", Description: "邮件提交"},
993: {Port: 993, ServiceName: "IMAPS", Description: "安全IMAP"},
995: {Port: 995, ServiceName: "POP3S", Description: "安全POP3"},
// FTP
20: {Port: 20, ServiceName: "FTP-Data", Description: "文件传输协议数据"},
21: {Port: 21, ServiceName: "FTP", Description: "文件传输协议"},
// DNS
53: {Port: 53, ServiceName: "DNS", Description: "域名系统"},
// DHCP
67: {Port: 67, ServiceName: "DHCP-Server", Description: "动态主机配置协议服务器"},
68: {Port: 68, ServiceName: "DHCP-Client", Description: "动态主机配置协议客户端"},
// NTP
123: {Port: 123, ServiceName: "NTP", Description: "网络时间协议"},
// SNMP
161: {Port: 161, ServiceName: "SNMP", Description: "简单网络管理协议"},
162: {Port: 162, ServiceName: "SNMP-Trap", Description: "SNMP陷阱"},
// LDAP
389: {Port: 389, ServiceName: "LDAP", Description: "轻量目录访问协议"},
636: {Port: 636, ServiceName: "LDAPS", Description: "安全LDAP"},
// NAS相关服务
139: {Port: 139, ServiceName: "NetBIOS-SSN", Description: "NetBIOS会话服务"},
445: {Port: 445, ServiceName: "SMB", Description: "服务器消息块"},
548: {Port: 548, ServiceName: "AFP", Description: "Apple文件协议"},
2049: {Port: 2049, ServiceName: "NFS", Description: "网络文件系统"},
// 开发工具
9090: {Port: 9090, ServiceName: "Prometheus", Description: "Prometheus监控系统", IsHTTP: true},
5601: {Port: 5601, ServiceName: "Kibana", Description: "Kibana可视化平台", IsHTTP: true},
9200: {Port: 9200, ServiceName: "Elasticsearch", Description: "Elasticsearch搜索引擎", IsHTTP: true},
8888: {Port: 8888, ServiceName: "Jupyter", Description: "Jupyter Notebook", IsHTTP: true},
4040: {Port: 4040, ServiceName: "Spark UI", Description: "Apache Spark Web UI", IsHTTP: true},
// 消息队列
5672: {Port: 5672, ServiceName: "RabbitMQ", Description: "RabbitMQ消息队列"},
15672: {Port: 15672, ServiceName: "RabbitMQ管理", Description: "RabbitMQ管理界面", IsHTTP: true},
9092: {Port: 9092, ServiceName: "Kafka", Description: "Apache Kafka"},
// 容器和编排
2375: {Port: 2375, ServiceName: "Docker", Description: "Docker守护进程(非安全)"},
2376: {Port: 2376, ServiceName: "Docker", Description: "Docker守护进程(TLS)"},
6443: {Port: 6443, ServiceName: "Kubernetes API", Description: "Kubernetes API服务器"},
10250: {Port: 10250, ServiceName: "kubelet", Description: "Kubernetes kubelet"},
// Synology NAS特定端口
5000: {Port: 5000, ServiceName: "DSM", Description: "Synology DiskStation Manager", IsHTTP: true},
5001: {Port: 5001, ServiceName: "DSM (HTTPS)", Description: "Synology DiskStation Manager", IsHTTPS: true},
6690: {Port: 6690, ServiceName: "Cloud Station", Description: "Synology Cloud Station"},
9117: {Port: 9117, ServiceName: "Jackett", Description: "Jackett种子搜索", IsHTTP: true},
8989: {Port: 8989, ServiceName: "Sonarr", Description: "Sonarr自动下载", IsHTTP: true},
7878: {Port: 7878, ServiceName: "Radarr", Description: "Radarr电影下载", IsHTTP: true},
8686: {Port: 8686, ServiceName: "Lidarr", Description: "Lidarr音乐下载", IsHTTP: true},
}
}

324
internal/web/server.go Normal file
View File

@@ -0,0 +1,324 @@
package web
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"github.com/miaomint/port-manager/internal/scanner"
"github.com/miaomint/port-manager/internal/service"
)
// Server Web服务器
type Server struct {
scanner *scanner.PortScanner
identifier *service.ServiceIdentifier
}
// NewServer 创建新的Web服务器
func NewServer() *Server {
return &Server{
scanner: scanner.NewPortScanner(true, false), // 只显示监听端口
identifier: service.NewServiceIdentifier(),
}
}
// Start 启动Web服务器
func (s *Server) Start(port int) error {
mux := http.NewServeMux()
// 静态文件路由
mux.HandleFunc("/", s.indexHandler)
mux.HandleFunc("/api/ports", s.portsAPIHandler)
mux.HandleFunc("/api/random-port", s.randomPortHandler)
mux.HandleFunc("/api/scan-range", s.scanRangeHandler)
fmt.Printf("端口管理器启动在: http://localhost:%d\n", port)
return http.ListenAndServe(fmt.Sprintf(":%d", port), mux)
}
// indexHandler 主页处理器
func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) {
tmpl := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>端口管理器</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
margin-bottom: 30px;
}
.controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
.random-port {
background: #28a745;
}
.random-port:hover {
background: #1e7e34;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f8f9fa;
font-weight: 600;
}
.service-link {
color: #007bff;
text-decoration: none;
}
.service-link:hover {
text-decoration: underline;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.status-active {
color: #28a745;
font-weight: bold;
}
.port-number {
font-family: monospace;
font-weight: bold;
}
.protocol-badge {
background: #6c757d;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.8em;
}
.random-result {
background: #d4edda;
border: 1px solid #c3e6cb;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 NAS 端口管理器</h1>
<div class="controls">
<button onclick="scanPorts()">🔍 扫描端口</button>
<button onclick="generateRandomPort()" class="random-port">🎲 生成随机端口</button>
<input type="number" id="minPort" placeholder="最小端口" value="8000" style="width: 100px;">
<input type="number" id="maxPort" placeholder="最大端口" value="9999" style="width: 100px;">
</div>
<div id="randomResult"></div>
<div id="loading" class="loading" style="display: none;">
正在扫描端口...
</div>
<table id="portsTable" style="display: none;">
<thead>
<tr>
<th>端口</th>
<th>协议</th>
<th>状态</th>
<th>服务名称</th>
<th>快速访问</th>
</tr>
</thead>
<tbody id="portsBody">
</tbody>
</table>
</div>
<script>
async function scanPorts() {
document.getElementById('loading').style.display = 'block';
document.getElementById('portsTable').style.display = 'none';
try {
const response = await fetch('/api/ports');
const data = await response.json();
displayPorts(data.ports);
} catch (error) {
console.error('扫描失败:', error);
alert('扫描失败: ' + error.message);
} finally {
document.getElementById('loading').style.display = 'none';
}
}
async function generateRandomPort() {
const minPort = document.getElementById('minPort').value || 8000;
const maxPort = document.getElementById('maxPort').value || 9999;
try {
const response = await fetch('/api/random-port?min=' + minPort + '&max=' + maxPort);
const data = await response.json();
document.getElementById('randomResult').innerHTML =
'<strong>🎯 可用端口: <span class="port-number">' + data.port + '</span></strong>';
} catch (error) {
console.error('生成随机端口失败:', error);
alert('生成随机端口失败: ' + error.message);
}
}
function displayPorts(ports) {
const tbody = document.getElementById('portsBody');
tbody.innerHTML = '';
ports.forEach(port => {
const row = document.createElement('tr');
const serviceCell = port.serviceURL
? '<a href="' + port.serviceURL + '" target="_blank" class="service-link">🚀 打开服务</a>'
: '-';
row.innerHTML =
'<td class="port-number">' + port.port + '</td>' +
'<td><span class="protocol-badge">' + port.protocol.toUpperCase() + '</span></td>' +
'<td class="status-active">' + (port.state || 'ACTIVE') + '</td>' +
'<td>' + (port.serviceName || '未知服务') + '</td>' +
'<td>' + serviceCell + '</td>';
tbody.appendChild(row);
});
document.getElementById('portsTable').style.display = 'table';
}
// 页面加载完成后自动扫描
window.onload = function() {
scanPorts();
};
</script>
</body>
</html>`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, tmpl)
}
// portsAPIHandler 端口API处理器
func (s *Server) portsAPIHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
result, err := s.scanner.ScanPorts()
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
return
}
// 识别服务
for i := range result.Ports {
identified := s.identifier.IdentifyService(&result.Ports[i])
result.Ports[i] = *identified
}
json.NewEncoder(w).Encode(result)
}
// randomPortHandler 随机端口处理器
func (s *Server) randomPortHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
minPort := 8000
maxPort := 9999
if min := r.URL.Query().Get("min"); min != "" {
if p, err := strconv.Atoi(min); err == nil {
minPort = p
}
}
if max := r.URL.Query().Get("max"); max != "" {
if p, err := strconv.Atoi(max); err == nil {
maxPort = p
}
}
port, err := s.scanner.GenerateRandomPort(minPort, maxPort)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
return
}
response := map[string]int{"port": port}
json.NewEncoder(w).Encode(response)
}
// scanRangeHandler 扫描端口范围处理器
func (s *Server) scanRangeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
startStr := r.URL.Query().Get("start")
endStr := r.URL.Query().Get("end")
if startStr == "" || endStr == "" {
http.Error(w, `{"error": "需要提供start和end参数"}`, http.StatusBadRequest)
return
}
start, err := strconv.Atoi(startStr)
if err != nil {
http.Error(w, `{"error": "无效的start参数"}`, http.StatusBadRequest)
return
}
end, err := strconv.Atoi(endStr)
if err != nil {
http.Error(w, `{"error": "无效的end参数"}`, http.StatusBadRequest)
return
}
openPorts, err := s.scanner.ScanPortRange(start, end)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err.Error()), http.StatusInternalServerError)
return
}
response := map[string][]int{"openPorts": openPorts}
json.NewEncoder(w).Encode(response)
}