132 lines
3.7 KiB
Go
132 lines
3.7 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Service struct {
|
|
client *http.Client
|
|
}
|
|
|
|
type Result struct {
|
|
Name string `json:"name"`
|
|
Port int `json:"port"`
|
|
Host string `json:"host"`
|
|
Upstream string `json:"upstream"`
|
|
Detected bool `json:"detected"`
|
|
Platform string `json:"platform"`
|
|
Title string `json:"title,omitempty"`
|
|
Suggestion string `json:"suggestion,omitempty"`
|
|
}
|
|
|
|
type catalogEntry struct {
|
|
Name string
|
|
Port int
|
|
Platform string
|
|
Keywords []string
|
|
}
|
|
|
|
var catalog = []catalogEntry{
|
|
{Name: "飞牛 fnOS", Port: 5666, Platform: "fnos", Keywords: []string{"fnos", "飞牛", "fnos.cn"}},
|
|
{Name: "AList", Port: 5244, Platform: "alist", Keywords: []string{"alist"}},
|
|
{Name: "Jellyfin", Port: 8096, Platform: "jellyfin", Keywords: []string{"jellyfin"}},
|
|
{Name: "PhotoPrism", Port: 2342, Platform: "photoprism", Keywords: []string{"photoprism"}},
|
|
{Name: "qBittorrent", Port: 8080, Platform: "qbittorrent", Keywords: []string{"qbittorrent"}},
|
|
{Name: "Portainer", Port: 9000, Platform: "portainer", Keywords: []string{"portainer"}},
|
|
{Name: "Home Assistant", Port: 8123, Platform: "home-assistant", Keywords: []string{"home assistant", "homeassistant"}},
|
|
{Name: "Immich", Port: 2283, Platform: "immich", Keywords: []string{"immich"}},
|
|
{Name: "AdGuard Home", Port: 3000, Platform: "adguard-home", Keywords: []string{"adguard home", "adguardhome"}},
|
|
{Name: "Synology DSM", Port: 5000, Platform: "synology", Keywords: []string{"synology", "diskstation"}},
|
|
}
|
|
|
|
func New() *Service {
|
|
return &Service{
|
|
client: &http.Client{Timeout: 2 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (s *Service) Scan(ctx context.Context, host string) []Result {
|
|
host = strings.TrimSpace(host)
|
|
if host == "" {
|
|
host = "127.0.0.1"
|
|
}
|
|
out := make([]Result, 0, len(catalog))
|
|
for _, item := range catalog {
|
|
result := Result{
|
|
Name: item.Name,
|
|
Port: item.Port,
|
|
Host: host,
|
|
Upstream: fmt.Sprintf("http://%s:%d", host, item.Port),
|
|
Platform: item.Platform,
|
|
}
|
|
if !s.portOpen(ctx, host, item.Port) {
|
|
out = append(out, result)
|
|
continue
|
|
}
|
|
result.Detected = true
|
|
title, body := s.probeHTTP(ctx, host, item.Port)
|
|
result.Title = title
|
|
if s.matchPlatform(item, title, body) {
|
|
result.Platform = item.Platform
|
|
if item.Platform == "fnos" {
|
|
result.Suggestion = "检测到飞牛 fnOS 服务,可使用 nas.<域名> 创建反向代理"
|
|
}
|
|
}
|
|
out = append(out, result)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Service) portOpen(ctx context.Context, host string, port int) bool {
|
|
dialer := net.Dialer{Timeout: 800 * time.Millisecond}
|
|
conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", host, port))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
func (s *Service) probeHTTP(ctx context.Context, host string, port int) (string, string) {
|
|
url := fmt.Sprintf("http://%s:%d/", host, port)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
defer resp.Body.Close()
|
|
bodyBytes := make([]byte, 4096)
|
|
n, _ := resp.Body.Read(bodyBytes)
|
|
body := strings.ToLower(string(bodyBytes[:n]))
|
|
title := extractTitle(body)
|
|
return title, body
|
|
}
|
|
|
|
func (s *Service) matchPlatform(item catalogEntry, title, body string) bool {
|
|
content := strings.ToLower(title + " " + body)
|
|
for _, keyword := range item.Keywords {
|
|
if strings.Contains(content, strings.ToLower(keyword)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func extractTitle(body string) string {
|
|
lower := strings.ToLower(body)
|
|
start := strings.Index(lower, "<title>")
|
|
end := strings.Index(lower, "</title>")
|
|
if start >= 0 && end > start {
|
|
return strings.TrimSpace(body[start+7 : end])
|
|
}
|
|
return ""
|
|
}
|