index.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package site
  2. import (
  3. "net"
  4. "path/filepath"
  5. "regexp"
  6. "strconv"
  7. "strings"
  8. "github.com/0xJacky/Nginx-UI/internal/cache"
  9. "github.com/0xJacky/Nginx-UI/internal/upstream"
  10. )
  11. type SiteIndex struct {
  12. Path string
  13. Content string
  14. Urls []string
  15. ProxyTargets []ProxyTarget
  16. }
  17. var (
  18. IndexedSites = make(map[string]*SiteIndex)
  19. )
  20. func GetIndexedSite(path string) *SiteIndex {
  21. if site, ok := IndexedSites[path]; ok {
  22. return site
  23. }
  24. return &SiteIndex{}
  25. }
  26. func init() {
  27. cache.RegisterCallback(scanForSite)
  28. }
  29. func scanForSite(configPath string, content []byte) error {
  30. // Regular expressions for server_name and listen directives
  31. serverNameRegex := regexp.MustCompile(`(?m)server_name\s+([^;]+);`)
  32. listenRegex := regexp.MustCompile(`(?m)listen\s+([^;]+);`)
  33. returnRegex := regexp.MustCompile(`(?m)return\s+30[1-8]\s+https://`)
  34. // Find server blocks
  35. serverBlockRegex := regexp.MustCompile(`(?ms)server\s*\{[^\{]*((.*?\{.*?\})*?[^\}]*)\}`)
  36. serverBlocks := serverBlockRegex.FindAllSubmatch(content, -1)
  37. siteIndex := SiteIndex{
  38. Path: configPath,
  39. Content: string(content),
  40. Urls: []string{},
  41. ProxyTargets: []ProxyTarget{},
  42. }
  43. // Map to track hosts, their SSL status and port
  44. type hostInfo struct {
  45. hasSSL bool
  46. port int
  47. isPublic bool // Whether this is a public-facing port
  48. priority int // Higher priority for public ports
  49. hasRedirect bool // Whether this server block has HTTPS redirect
  50. }
  51. hostMap := make(map[string]hostInfo)
  52. for _, block := range serverBlocks {
  53. serverBlockContent := block[0]
  54. // Extract server_name values
  55. serverNameMatches := serverNameRegex.FindSubmatch(serverBlockContent)
  56. if len(serverNameMatches) < 2 {
  57. continue
  58. }
  59. // Get all server names
  60. serverNames := strings.Fields(string(serverNameMatches[1]))
  61. var validServerNames []string
  62. // Filter valid domain names and IPs
  63. for _, name := range serverNames {
  64. // Skip placeholder names
  65. if name == "_" || name == "localhost" {
  66. continue
  67. }
  68. // Check if it's a valid IP
  69. if net.ParseIP(name) != nil {
  70. validServerNames = append(validServerNames, name)
  71. continue
  72. }
  73. // Basic domain validation
  74. if isValidDomain(name) {
  75. validServerNames = append(validServerNames, name)
  76. }
  77. }
  78. if len(validServerNames) == 0 {
  79. continue
  80. }
  81. // Check if this server block has HTTPS redirect
  82. hasRedirect := returnRegex.Match(serverBlockContent)
  83. // Check if SSL is enabled and extract port
  84. listenMatches := listenRegex.FindAllSubmatch(serverBlockContent, -1)
  85. for _, match := range listenMatches {
  86. if len(match) >= 2 {
  87. listenValue := strings.TrimSpace(string(match[1]))
  88. hasSSL := strings.Contains(listenValue, "ssl")
  89. port := 80 // Default HTTP port
  90. isPublic := true
  91. priority := 1
  92. if hasSSL {
  93. port = 443 // Default HTTPS port
  94. priority = 3 // SSL has highest priority
  95. } else if hasRedirect {
  96. priority = 2 // HTTP with redirect has medium priority
  97. }
  98. // Parse different listen directive formats
  99. // Format examples:
  100. // - 80
  101. // - 443 ssl
  102. // - [::]:80
  103. // - 127.0.0.1:8443 ssl
  104. // - *:80
  105. // Remove extra parameters (ssl, http2, etc.) for parsing
  106. listenParts := strings.Fields(listenValue)
  107. if len(listenParts) > 0 {
  108. addressPart := listenParts[0]
  109. // Check if it's bound to a specific IP (not public)
  110. if strings.Contains(addressPart, "127.0.0.1") ||
  111. strings.Contains(addressPart, "localhost") {
  112. isPublic = false
  113. priority = 0 // Internal ports have lowest priority
  114. }
  115. // Extract port from various formats
  116. var extractedPort int
  117. var err error
  118. if strings.Contains(addressPart, ":") {
  119. // Handle IPv6 format [::]:port or IPv4 format ip:port
  120. if strings.HasPrefix(addressPart, "[") {
  121. // IPv6 format: [::]:80
  122. if colonIndex := strings.LastIndex(addressPart, ":"); colonIndex != -1 {
  123. portStr := addressPart[colonIndex+1:]
  124. extractedPort, err = strconv.Atoi(portStr)
  125. }
  126. } else {
  127. // IPv4 format: 127.0.0.1:8443 or *:80
  128. if colonIndex := strings.LastIndex(addressPart, ":"); colonIndex != -1 {
  129. portStr := addressPart[colonIndex+1:]
  130. extractedPort, err = strconv.Atoi(portStr)
  131. }
  132. }
  133. } else {
  134. // Just a port number: 80, 443
  135. extractedPort, err = strconv.Atoi(addressPart)
  136. }
  137. if err == nil && extractedPort > 0 {
  138. port = extractedPort
  139. }
  140. }
  141. // Update host map with SSL status and port, prioritizing public ports
  142. for _, name := range validServerNames {
  143. info, exists := hostMap[name]
  144. // Update if:
  145. // 1. Host doesn't exist yet
  146. // 2. New entry has higher priority (SSL > redirect > plain HTTP, public > private)
  147. // 3. Same priority but adding SSL
  148. if !exists ||
  149. priority > info.priority ||
  150. (priority == info.priority && hasSSL && !info.hasSSL) {
  151. hostMap[name] = hostInfo{
  152. hasSSL: hasSSL,
  153. port: port,
  154. isPublic: isPublic,
  155. priority: priority,
  156. hasRedirect: hasRedirect,
  157. }
  158. }
  159. }
  160. }
  161. }
  162. }
  163. // Generate URLs from the host map
  164. for host, info := range hostMap {
  165. // Skip internal/private addresses for URL generation
  166. if !info.isPublic {
  167. continue
  168. }
  169. protocol := "http"
  170. defaultPort := 80
  171. // If we have a redirect to HTTPS, prefer HTTPS URL
  172. if info.hasSSL || info.hasRedirect {
  173. protocol = "https"
  174. defaultPort = 443
  175. }
  176. url := protocol + "://" + host
  177. // Add port to URL if non-standard
  178. if info.port != defaultPort && info.hasSSL {
  179. // Only add port for SSL if it's not the default SSL port
  180. url += ":" + strconv.Itoa(info.port)
  181. } else if info.port != defaultPort && !info.hasSSL && !info.hasRedirect {
  182. // Add port for non-SSL, non-redirect cases
  183. url += ":" + strconv.Itoa(info.port)
  184. }
  185. siteIndex.Urls = append(siteIndex.Urls, url)
  186. }
  187. // Parse proxy targets from the configuration content
  188. siteIndex.ProxyTargets = upstream.ParseProxyTargetsFromRawContent(string(content))
  189. // Only store if we found valid URLs or proxy targets
  190. if len(siteIndex.Urls) > 0 || len(siteIndex.ProxyTargets) > 0 {
  191. IndexedSites[filepath.Base(configPath)] = &siteIndex
  192. }
  193. return nil
  194. }
  195. // isValidDomain performs a basic validation of domain names
  196. func isValidDomain(domain string) bool {
  197. // Basic validation: contains at least one dot and no spaces
  198. return strings.Contains(domain, ".") && !strings.Contains(domain, " ")
  199. }