1
0

index.go 6.5 KB

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