index.go 6.3 KB

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