123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- package site
- import (
- "net"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "github.com/0xJacky/Nginx-UI/internal/cache"
- "github.com/0xJacky/Nginx-UI/internal/upstream"
- )
- type SiteIndex struct {
- Path string
- Content string
- Urls []string
- ProxyTargets []ProxyTarget
- }
- var (
- IndexedSites = make(map[string]*SiteIndex)
- )
- func GetIndexedSite(path string) *SiteIndex {
- if site, ok := IndexedSites[path]; ok {
- return site
- }
- return &SiteIndex{}
- }
- func init() {
- cache.RegisterCallback(scanForSite)
- }
- func scanForSite(configPath string, content []byte) error {
- // Regular expressions for server_name and listen directives
- serverNameRegex := regexp.MustCompile(`(?m)server_name\s+([^;]+);`)
- listenRegex := regexp.MustCompile(`(?m)listen\s+([^;]+);`)
- returnRegex := regexp.MustCompile(`(?m)return\s+30[1-8]\s+https://`)
- // Find server blocks
- serverBlockRegex := regexp.MustCompile(`(?ms)server\s*\{[^\{]*((.*?\{.*?\})*?[^\}]*)\}`)
- serverBlocks := serverBlockRegex.FindAllSubmatch(content, -1)
- siteIndex := SiteIndex{
- Path: configPath,
- Content: string(content),
- Urls: []string{},
- ProxyTargets: []ProxyTarget{},
- }
- // Map to track hosts, their SSL status and port
- type hostInfo struct {
- hasSSL bool
- port int
- isPublic bool // Whether this is a public-facing port
- priority int // Higher priority for public ports
- hasRedirect bool // Whether this server block has HTTPS redirect
- }
- hostMap := make(map[string]hostInfo)
- for _, block := range serverBlocks {
- serverBlockContent := block[0]
- // Extract server_name values
- serverNameMatches := serverNameRegex.FindSubmatch(serverBlockContent)
- if len(serverNameMatches) < 2 {
- continue
- }
- // Get all server names
- serverNames := strings.Fields(string(serverNameMatches[1]))
- var validServerNames []string
- // Filter valid domain names and IPs
- for _, name := range serverNames {
- // Skip placeholder names
- if name == "_" || name == "localhost" {
- continue
- }
- // Check if it's a valid IP
- if net.ParseIP(name) != nil {
- validServerNames = append(validServerNames, name)
- continue
- }
- // Basic domain validation
- if isValidDomain(name) {
- validServerNames = append(validServerNames, name)
- }
- }
- if len(validServerNames) == 0 {
- continue
- }
- // Check if this server block has HTTPS redirect
- hasRedirect := returnRegex.Match(serverBlockContent)
- // Check if SSL is enabled and extract port
- listenMatches := listenRegex.FindAllSubmatch(serverBlockContent, -1)
- for _, match := range listenMatches {
- if len(match) >= 2 {
- listenValue := strings.TrimSpace(string(match[1]))
- hasSSL := strings.Contains(listenValue, "ssl")
- port := 80 // Default HTTP port
- isPublic := true
- priority := 1
- if hasSSL {
- port = 443 // Default HTTPS port
- priority = 3 // SSL has highest priority
- } else if hasRedirect {
- priority = 2 // HTTP with redirect has medium priority
- }
- // Parse different listen directive formats
- // Format examples:
- // - 80
- // - 443 ssl
- // - [::]:80
- // - 127.0.0.1:8443 ssl
- // - *:80
- // Remove extra parameters (ssl, http2, etc.) for parsing
- listenParts := strings.Fields(listenValue)
- if len(listenParts) > 0 {
- addressPart := listenParts[0]
- // Check if it's bound to a specific IP (not public)
- if strings.Contains(addressPart, "127.0.0.1") ||
- strings.Contains(addressPart, "localhost") {
- isPublic = false
- priority = 0 // Internal ports have lowest priority
- }
- // Extract port from various formats
- var extractedPort int
- var err error
- if strings.Contains(addressPart, ":") {
- // Handle IPv6 format [::]:port or IPv4 format ip:port
- if strings.HasPrefix(addressPart, "[") {
- // IPv6 format: [::]:80
- if colonIndex := strings.LastIndex(addressPart, ":"); colonIndex != -1 {
- portStr := addressPart[colonIndex+1:]
- extractedPort, err = strconv.Atoi(portStr)
- }
- } else {
- // IPv4 format: 127.0.0.1:8443 or *:80
- if colonIndex := strings.LastIndex(addressPart, ":"); colonIndex != -1 {
- portStr := addressPart[colonIndex+1:]
- extractedPort, err = strconv.Atoi(portStr)
- }
- }
- } else {
- // Just a port number: 80, 443
- extractedPort, err = strconv.Atoi(addressPart)
- }
- if err == nil && extractedPort > 0 {
- port = extractedPort
- }
- }
- // Update host map with SSL status and port, prioritizing public ports
- for _, name := range validServerNames {
- info, exists := hostMap[name]
- // Update if:
- // 1. Host doesn't exist yet
- // 2. New entry has higher priority (SSL > redirect > plain HTTP, public > private)
- // 3. Same priority but adding SSL
- if !exists ||
- priority > info.priority ||
- (priority == info.priority && hasSSL && !info.hasSSL) {
- hostMap[name] = hostInfo{
- hasSSL: hasSSL,
- port: port,
- isPublic: isPublic,
- priority: priority,
- hasRedirect: hasRedirect,
- }
- }
- }
- }
- }
- }
- // Generate URLs from the host map
- for host, info := range hostMap {
- // Skip internal/private addresses for URL generation
- if !info.isPublic {
- continue
- }
- protocol := "http"
- defaultPort := 80
- // If we have a redirect to HTTPS, prefer HTTPS URL
- if info.hasSSL || info.hasRedirect {
- protocol = "https"
- defaultPort = 443
- }
- url := protocol + "://" + host
- // Add port to URL if non-standard
- if info.port != defaultPort && info.hasSSL {
- // Only add port for SSL if it's not the default SSL port
- url += ":" + strconv.Itoa(info.port)
- } else if info.port != defaultPort && !info.hasSSL && !info.hasRedirect {
- // Add port for non-SSL, non-redirect cases
- url += ":" + strconv.Itoa(info.port)
- }
- siteIndex.Urls = append(siteIndex.Urls, url)
- }
- // Parse proxy targets from the configuration content
- siteIndex.ProxyTargets = upstream.ParseProxyTargetsFromRawContent(string(content))
- // Only store if we found valid URLs or proxy targets
- if len(siteIndex.Urls) > 0 || len(siteIndex.ProxyTargets) > 0 {
- IndexedSites[filepath.Base(configPath)] = &siteIndex
- }
- return nil
- }
- // isValidDomain performs a basic validation of domain names
- func isValidDomain(domain string) bool {
- // Basic validation: contains at least one dot and no spaces
- return strings.Contains(domain, ".") && !strings.Contains(domain, " ")
- }
|