first commit
This commit is contained in:
57
.gitignore
vendored
Normal file
57
.gitignore
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
# 编译输出
|
||||
bin/
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Go 测试输出
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Go 依赖模块
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# Go 构建缓存
|
||||
.cache/
|
||||
|
||||
# IDE 文件
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.tmp/
|
||||
|
||||
# 环境变量文件
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# 编译器输出
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug 文件
|
||||
debug
|
||||
debug.test
|
||||
main
|
||||
|
||||
# Vendor 目录 (如果使用 vendor)
|
||||
vendor/
|
||||
100
Makefile
Normal file
100
Makefile
Normal file
@@ -0,0 +1,100 @@
|
||||
# Port Manager Makefile
|
||||
|
||||
.PHONY: build run clean test help scan random install
|
||||
|
||||
# 默认目标
|
||||
all: build
|
||||
|
||||
# 构建可执行文件
|
||||
build:
|
||||
@echo "🔨 构建 Port Manager..."
|
||||
@mkdir -p bin
|
||||
@go build -o bin/port-manager cmd/port-manager/main.go
|
||||
@echo "✅ 构建完成: bin/port-manager"
|
||||
|
||||
# 运行Web服务器(默认端口8080)
|
||||
run: build
|
||||
@echo "🚀 启动 Port Manager Web 界面..."
|
||||
@./bin/port-manager
|
||||
|
||||
# 运行Web服务器在指定端口
|
||||
run-port: build
|
||||
@echo "🚀 启动 Port Manager Web 界面 (端口: $(PORT))..."
|
||||
@./bin/port-manager -port $(PORT)
|
||||
|
||||
# 仅扫描端口
|
||||
scan: build
|
||||
@./bin/port-manager -scan
|
||||
|
||||
# 生成随机端口
|
||||
random: build
|
||||
@./bin/port-manager -random
|
||||
|
||||
# 生成指定范围的随机端口
|
||||
random-range: build
|
||||
@./bin/port-manager -random -min $(MIN) -max $(MAX)
|
||||
|
||||
# 清理构建文件
|
||||
clean:
|
||||
@echo "🧹 清理构建文件..."
|
||||
@rm -rf bin/
|
||||
@go clean
|
||||
@echo "✅ 清理完成"
|
||||
|
||||
# 运行测试
|
||||
test:
|
||||
@echo "🧪 运行测试..."
|
||||
@go test ./...
|
||||
|
||||
# 格式化代码
|
||||
fmt:
|
||||
@echo "📝 格式化代码..."
|
||||
@go fmt ./...
|
||||
|
||||
# 代码检查
|
||||
vet:
|
||||
@echo "🔍 代码检查..."
|
||||
@go vet ./...
|
||||
|
||||
# 更新依赖
|
||||
mod-tidy:
|
||||
@echo "📦 更新依赖..."
|
||||
@go mod tidy
|
||||
|
||||
# 安装到系统
|
||||
install: build
|
||||
@echo "📥 安装到系统..."
|
||||
@sudo cp bin/port-manager /usr/local/bin/
|
||||
@echo "✅ 安装完成: /usr/local/bin/port-manager"
|
||||
|
||||
# 显示帮助
|
||||
help:
|
||||
@echo "Port Manager - NAS 端口管理工具"
|
||||
@echo ""
|
||||
@echo "可用命令:"
|
||||
@echo " make build - 构建可执行文件"
|
||||
@echo " make run - 运行Web界面 (默认端口8080)"
|
||||
@echo " make run-port PORT=9000 - 在指定端口运行Web界面"
|
||||
@echo " make scan - 扫描端口并显示结果"
|
||||
@echo " make random - 生成随机可用端口"
|
||||
@echo " make random-range MIN=3000 MAX=4000 - 生成指定范围随机端口"
|
||||
@echo " make clean - 清理构建文件"
|
||||
@echo " make test - 运行测试"
|
||||
@echo " make fmt - 格式化代码"
|
||||
@echo " make vet - 代码检查"
|
||||
@echo " make mod-tidy - 更新依赖"
|
||||
@echo " make install - 安装到系统"
|
||||
@echo " make help - 显示此帮助"
|
||||
@echo ""
|
||||
@echo "示例:"
|
||||
@echo " make run # 启动Web界面"
|
||||
@echo " make run-port PORT=9000 # 在端口9000启动"
|
||||
@echo " make random-range MIN=3000 MAX=4000 # 生成3000-4000范围内的随机端口"
|
||||
|
||||
# 开发环境设置
|
||||
dev-setup:
|
||||
@echo "🛠️ 设置开发环境..."
|
||||
@go mod tidy
|
||||
@go fmt ./...
|
||||
@go vet ./...
|
||||
@echo "✅ 开发环境设置完成"
|
||||
145
README.md
Normal file
145
README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Port Manager
|
||||
|
||||
一个用于 NAS 的端口管理工具,可以很方便的检查系统当前已经占用的端口,和识别服务。可以用作服务的快速入口(导航)也可以作为选择困难症的随机端口生成器。
|
||||
|
||||
## 主要功能
|
||||
|
||||
- 🔍 扫描本地已经占用的端口
|
||||
- 🎯 识别端口所对应的服务
|
||||
- 🚀 快速访问服务 (Web界面导航)
|
||||
- 🎲 随机生成一个非占用的端口
|
||||
- 📱 现代化的Web界面
|
||||
- 💻 命令行模式支持
|
||||
|
||||
## 安装和使用
|
||||
|
||||
### 从源码构建
|
||||
|
||||
```bash
|
||||
git clone https://github.com/miaomint/port-manager.git
|
||||
cd port-manager
|
||||
go mod tidy
|
||||
go build -o bin/port-manager cmd/port-manager/main.go
|
||||
```
|
||||
|
||||
### 使用方法
|
||||
|
||||
#### 1. Web界面模式(推荐)
|
||||
|
||||
```bash
|
||||
# 默认在8080端口启动Web界面
|
||||
./bin/port-manager
|
||||
|
||||
# 指定端口启动
|
||||
./bin/port-manager -port 9000
|
||||
```
|
||||
|
||||
然后在浏览器中访问 `http://localhost:8080`
|
||||
|
||||
#### 2. 命令行模式
|
||||
|
||||
```bash
|
||||
# 扫描端口并在终端显示
|
||||
./bin/port-manager -scan
|
||||
|
||||
# 生成随机可用端口
|
||||
./bin/port-manager -random
|
||||
|
||||
# 在指定范围内生成随机端口
|
||||
./bin/port-manager -random -min 3000 -max 4000
|
||||
|
||||
# 查看帮助
|
||||
./bin/port-manager -help
|
||||
```
|
||||
|
||||
## 支持的服务识别
|
||||
|
||||
本工具内置了常见服务的端口识别,包括:
|
||||
|
||||
### Web服务
|
||||
- HTTP (80, 8080, 3000, 4000, 5000, 8000)
|
||||
- HTTPS (443, 8443, 5001)
|
||||
- 开发服务器 (React, Node.js, Flask, Django等)
|
||||
|
||||
### 数据库
|
||||
- MySQL (3306)
|
||||
- PostgreSQL (5432)
|
||||
- MongoDB (27017)
|
||||
- Redis (6379)
|
||||
- Memcached (11211)
|
||||
|
||||
### NAS相关
|
||||
- Synology DSM (5000, 5001)
|
||||
- SMB/CIFS (445)
|
||||
- AFP (548)
|
||||
- NFS (2049)
|
||||
- 常见下载工具 (Sonarr, Radarr, Jackett等)
|
||||
|
||||
### 开发工具
|
||||
- Grafana (3001)
|
||||
- Prometheus (9090)
|
||||
- Elasticsearch (9200)
|
||||
- Kibana (5601)
|
||||
- Jupyter (8888)
|
||||
|
||||
### 其他
|
||||
- SSH (22)
|
||||
- FTP (21)
|
||||
- DNS (53)
|
||||
- 等等...
|
||||
|
||||
## 项目结构
|
||||
|
||||
本项目遵循标准的 Go 项目结构:
|
||||
|
||||
```
|
||||
port-manager/
|
||||
├── cmd/
|
||||
│ └── port-manager/ # 主程序入口
|
||||
│ └── main.go
|
||||
├── internal/ # 内部包,不对外暴露
|
||||
│ ├── scanner/ # 端口扫描功能
|
||||
│ ├── service/ # 服务识别功能
|
||||
│ └── web/ # Web服务器
|
||||
├── pkg/ # 可被外部使用的包
|
||||
│ └── types/ # 类型定义
|
||||
├── go.mod
|
||||
├── go.sum
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Web界面特性
|
||||
|
||||
- 📊 实时端口扫描
|
||||
- 🎨 现代化响应式设计
|
||||
- 🔗 一键访问已识别的Web服务
|
||||
- 🎲 随机端口生成器
|
||||
- 📱 移动端友好
|
||||
- ⚡ 快速加载
|
||||
|
||||
## 开发
|
||||
|
||||
### 依赖
|
||||
|
||||
- Go 1.21+
|
||||
- 仅使用标准库,无外部依赖
|
||||
|
||||
### 运行开发版本
|
||||
|
||||
```bash
|
||||
go run cmd/port-manager/main.go
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
139
cmd/port-manager/main.go
Normal file
139
cmd/port-manager/main.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/miaomint/port-manager/internal/scanner"
|
||||
"github.com/miaomint/port-manager/internal/service"
|
||||
"github.com/miaomint/port-manager/internal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
webPort = flag.Int("port", 8080, "Web服务器端口")
|
||||
scanOnly = flag.Bool("scan", false, "仅扫描端口并输出结果")
|
||||
randomPort = flag.Bool("random", false, "生成随机可用端口")
|
||||
minPort = flag.Int("min", 8000, "随机端口最小值")
|
||||
maxPort = flag.Int("max", 9999, "随机端口最大值")
|
||||
showHelp = flag.Bool("help", false, "显示帮助信息")
|
||||
)
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *showHelp {
|
||||
showUsage()
|
||||
return
|
||||
}
|
||||
|
||||
// 创建扫描器和识别器
|
||||
portScanner := scanner.NewPortScanner(true, false)
|
||||
serviceIdentifier := service.NewServiceIdentifier()
|
||||
|
||||
// 仅扫描模式
|
||||
if *scanOnly {
|
||||
fmt.Println("🔍 正在扫描本地端口...")
|
||||
result, err := portScanner.ScanPorts()
|
||||
if err != nil {
|
||||
log.Fatalf("扫描失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\n📊 扫描结果 (共发现 %d 个端口):\n", result.TotalPorts)
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
fmt.Printf("%-8s %-10s %-12s %-30s %-s\n", "端口", "协议", "状态", "服务名称", "访问地址")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
for _, port := range result.Ports {
|
||||
identified := serviceIdentifier.IdentifyService(&port)
|
||||
serviceURL := identified.ServiceURL
|
||||
if serviceURL == "" {
|
||||
serviceURL = "-"
|
||||
}
|
||||
fmt.Printf("%-8d %-10s %-12s %-30s %-s\n",
|
||||
identified.Port,
|
||||
identified.Protocol,
|
||||
identified.State,
|
||||
identified.ServiceName,
|
||||
serviceURL,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 随机端口生成模式
|
||||
if *randomPort {
|
||||
fmt.Printf("🎲 正在生成随机可用端口 (范围: %d-%d)...\n", *minPort, *maxPort)
|
||||
port, err := portScanner.GenerateRandomPort(*minPort, *maxPort)
|
||||
if err != nil {
|
||||
log.Fatalf("生成随机端口失败: %v", err)
|
||||
}
|
||||
fmt.Printf("🎯 可用端口: %d\n", port)
|
||||
return
|
||||
}
|
||||
|
||||
// Web服务器模式(默认)
|
||||
fmt.Println("🚀 启动端口管理器...")
|
||||
fmt.Printf("📱 Web界面: http://localhost:%d\n", *webPort)
|
||||
fmt.Println("💡 使用 --help 查看更多选项")
|
||||
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||
|
||||
server := web.NewServer()
|
||||
if err := server.Start(*webPort); err != nil {
|
||||
log.Fatalf("启动Web服务器失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func showUsage() {
|
||||
fmt.Println(`
|
||||
🔌 NAS 端口管理器
|
||||
================
|
||||
|
||||
一个用于NAS的端口管理工具,可以方便地检查系统当前已经占用的端口,识别服务,
|
||||
提供快速访问入口,也可以作为随机端口生成器。
|
||||
|
||||
使用方法:
|
||||
port-manager [选项]
|
||||
|
||||
选项:
|
||||
-port int
|
||||
Web服务器端口 (默认: 8080)
|
||||
-scan
|
||||
仅扫描端口并输出结果,不启动Web服务器
|
||||
-random
|
||||
生成一个随机可用端口
|
||||
-min int
|
||||
随机端口最小值 (默认: 8000)
|
||||
-max int
|
||||
随机端口最大值 (默认: 9999)
|
||||
-help
|
||||
显示此帮助信息
|
||||
|
||||
示例:
|
||||
# 启动Web界面 (默认端口8080)
|
||||
port-manager
|
||||
|
||||
# 在指定端口启动Web界面
|
||||
port-manager -port 9000
|
||||
|
||||
# 仅扫描端口并输出到终端
|
||||
port-manager -scan
|
||||
|
||||
# 生成随机可用端口
|
||||
port-manager -random
|
||||
|
||||
# 在指定范围内生成随机端口
|
||||
port-manager -random -min 3000 -max 4000
|
||||
|
||||
功能特性:
|
||||
✅ 扫描本地已占用端口
|
||||
✅ 识别端口对应的服务
|
||||
✅ 提供Web界面快速访问服务
|
||||
✅ 生成随机可用端口
|
||||
✅ 支持常见NAS服务识别
|
||||
✅ 响应式Web界面设计
|
||||
|
||||
更多信息:
|
||||
项目地址: https://github.com/miaomint/port-manager
|
||||
`)
|
||||
}
|
||||
218
internal/scanner/port_scanner.go
Normal file
218
internal/scanner/port_scanner.go
Normal 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("无法找到可用端口")
|
||||
}
|
||||
180
internal/service/identifier.go
Normal file
180
internal/service/identifier.go
Normal 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
324
internal/web/server.go
Normal 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)
|
||||
}
|
||||
31
pkg/types/port.go
Normal file
31
pkg/types/port.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// PortInfo 表示端口信息
|
||||
type PortInfo struct {
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"` // tcp, udp
|
||||
State string `json:"state"` // LISTEN, ESTABLISHED, etc.
|
||||
Process string `json:"process"` // 进程名称
|
||||
PID int `json:"pid"` // 进程ID
|
||||
ServiceName string `json:"serviceName"` // 识别出的服务名称
|
||||
ServiceURL string `json:"serviceURL"` // 服务访问URL(如果是HTTP服务)
|
||||
LastSeen time.Time `json:"lastSeen"` // 最后发现时间
|
||||
}
|
||||
|
||||
// ScanResult 表示扫描结果
|
||||
type ScanResult struct {
|
||||
Ports []PortInfo `json:"ports"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
TotalPorts int `json:"totalPorts"`
|
||||
}
|
||||
|
||||
// ServiceMapping 表示端口到服务的映射
|
||||
type ServiceMapping struct {
|
||||
Port int `json:"port"`
|
||||
ServiceName string `json:"serviceName"`
|
||||
Description string `json:"description"`
|
||||
IsHTTP bool `json:"isHTTP"`
|
||||
IsHTTPS bool `json:"isHTTPS"`
|
||||
}
|
||||
Reference in New Issue
Block a user