proxy_parser.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. package upstream
  2. import (
  3. "net/url"
  4. "regexp"
  5. "strings"
  6. "github.com/0xJacky/Nginx-UI/internal/nginx"
  7. "github.com/0xJacky/Nginx-UI/settings"
  8. )
  9. // ProxyTarget represents a proxy destination
  10. type ProxyTarget struct {
  11. Host string `json:"host"`
  12. Port string `json:"port"`
  13. Type string `json:"type"` // "proxy_pass" or "upstream"
  14. Resolver string `json:"resolver"` // DNS resolver address (e.g., "127.0.0.1:8600")
  15. IsConsul bool `json:"is_consul"` // Whether this is a consul service discovery target
  16. ServiceURL string `json:"service_url"` // Full service URL for consul (e.g., "service.consul service=redacted-net resolve")
  17. }
  18. // UpstreamContext contains upstream-level configuration
  19. type UpstreamContext struct {
  20. Name string
  21. Resolver string
  22. }
  23. // ParseProxyTargetsFromRawContent parses proxy targets from raw nginx configuration content
  24. func ParseProxyTargetsFromRawContent(content string) []ProxyTarget {
  25. var targets []ProxyTarget
  26. // First, collect all upstream names and their contexts
  27. upstreamNames := make(map[string]bool)
  28. upstreamContexts := make(map[string]*UpstreamContext)
  29. upstreamRegex := regexp.MustCompile(`(?s)upstream\s+([^\s]+)\s*\{([^}]+)\}`)
  30. upstreamMatches := upstreamRegex.FindAllStringSubmatch(content, -1)
  31. // Parse upstream blocks and collect upstream names
  32. for _, match := range upstreamMatches {
  33. if len(match) >= 3 {
  34. upstreamName := match[1]
  35. upstreamNames[upstreamName] = true
  36. upstreamContent := match[2]
  37. // Create upstream context
  38. ctx := &UpstreamContext{
  39. Name: upstreamName,
  40. }
  41. // Extract resolver information from upstream block
  42. resolverRegex := regexp.MustCompile(`(?m)^\s*resolver\s+([^;]+);`)
  43. if resolverMatch := resolverRegex.FindStringSubmatch(upstreamContent); len(resolverMatch) >= 2 {
  44. // Parse resolver directive (e.g., "127.0.0.1:8600 valid=5s ipv6=off")
  45. resolverParts := strings.Fields(resolverMatch[1])
  46. if len(resolverParts) > 0 {
  47. ctx.Resolver = resolverParts[0] // Take the first part as resolver address
  48. }
  49. }
  50. upstreamContexts[upstreamName] = ctx
  51. serverRegex := regexp.MustCompile(`(?m)^\s*server\s+([^;]+);`)
  52. serverMatches := serverRegex.FindAllStringSubmatch(upstreamContent, -1)
  53. for _, serverMatch := range serverMatches {
  54. if len(serverMatch) >= 2 {
  55. target := parseServerAddress(strings.TrimSpace(serverMatch[1]), "upstream", ctx)
  56. if target.Host != "" {
  57. targets = append(targets, target)
  58. }
  59. }
  60. }
  61. }
  62. }
  63. // Parse proxy_pass directives, but skip upstream references
  64. proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
  65. proxyMatches := proxyPassRegex.FindAllStringSubmatch(content, -1)
  66. for _, match := range proxyMatches {
  67. if len(match) >= 2 {
  68. proxyPassURL := strings.TrimSpace(match[1])
  69. // Skip if this proxy_pass references an upstream
  70. if !isUpstreamReference(proxyPassURL, upstreamNames) {
  71. target := parseProxyPassURL(proxyPassURL)
  72. if target.Host != "" {
  73. targets = append(targets, target)
  74. }
  75. }
  76. }
  77. }
  78. return deduplicateTargets(targets)
  79. }
  80. // parseUpstreamServers extracts server addresses from upstream blocks
  81. func parseUpstreamServers(upstream *nginx.NgxUpstream) []ProxyTarget {
  82. var targets []ProxyTarget
  83. // Create upstream context for this upstream block
  84. ctx := &UpstreamContext{
  85. Name: upstream.Name,
  86. }
  87. // Extract resolver from upstream directives
  88. for _, directive := range upstream.Directives {
  89. if directive.Directive == "resolver" {
  90. resolverParts := strings.Fields(directive.Params)
  91. if len(resolverParts) > 0 {
  92. ctx.Resolver = resolverParts[0]
  93. }
  94. }
  95. }
  96. for _, directive := range upstream.Directives {
  97. if directive.Directive == "server" {
  98. target := parseServerAddress(directive.Params, "upstream", ctx)
  99. if target.Host != "" {
  100. targets = append(targets, target)
  101. }
  102. }
  103. }
  104. return targets
  105. }
  106. // parseLocationProxyPass extracts proxy_pass from location content
  107. func parseLocationProxyPass(content string) []ProxyTarget {
  108. var targets []ProxyTarget
  109. // Use regex to find proxy_pass directives
  110. proxyPassRegex := regexp.MustCompile(`(?m)^\s*proxy_pass\s+([^;]+);`)
  111. matches := proxyPassRegex.FindAllStringSubmatch(content, -1)
  112. for _, match := range matches {
  113. if len(match) >= 2 {
  114. target := parseProxyPassURL(strings.TrimSpace(match[1]))
  115. if target.Host != "" {
  116. targets = append(targets, target)
  117. }
  118. }
  119. }
  120. return targets
  121. }
  122. // parseProxyPassURL parses a proxy_pass URL and extracts host and port
  123. func parseProxyPassURL(proxyPass string) ProxyTarget {
  124. proxyPass = strings.TrimSpace(proxyPass)
  125. // Skip URLs that contain Nginx variables
  126. if strings.Contains(proxyPass, "$") {
  127. return ProxyTarget{}
  128. }
  129. // Handle HTTP/HTTPS URLs (e.g., "http://backend")
  130. if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
  131. if parsedURL, err := url.Parse(proxyPass); err == nil {
  132. host := parsedURL.Hostname()
  133. port := parsedURL.Port()
  134. // Set default ports if not specified
  135. if port == "" {
  136. if parsedURL.Scheme == "https" {
  137. port = "443"
  138. } else {
  139. port = "80"
  140. }
  141. }
  142. // Skip if this is the HTTP challenge port used by Let's Encrypt
  143. if host == "127.0.0.1" && port == settings.CertSettings.HTTPChallengePort {
  144. return ProxyTarget{}
  145. }
  146. return ProxyTarget{
  147. Host: host,
  148. Port: port,
  149. Type: "proxy_pass",
  150. }
  151. }
  152. }
  153. // Handle direct address format for stream module (e.g., "127.0.0.1:8080", "backend.example.com:12345")
  154. // This is used in stream configurations where proxy_pass doesn't require a protocol
  155. if !strings.Contains(proxyPass, "://") {
  156. target := parseServerAddress(proxyPass, "proxy_pass", nil) // No upstream context for this function
  157. // Skip if this is the HTTP challenge port used by Let's Encrypt
  158. if target.Host == "127.0.0.1" && target.Port == settings.CertSettings.HTTPChallengePort {
  159. return ProxyTarget{}
  160. }
  161. return target
  162. }
  163. return ProxyTarget{}
  164. }
  165. // parseServerAddress parses upstream server address with upstream context
  166. func parseServerAddress(serverAddr string, targetType string, ctx *UpstreamContext) ProxyTarget {
  167. serverAddr = strings.TrimSpace(serverAddr)
  168. // Remove additional parameters (weight, max_fails, etc.)
  169. parts := strings.Fields(serverAddr)
  170. if len(parts) == 0 {
  171. return ProxyTarget{}
  172. }
  173. addr := parts[0]
  174. target := ProxyTarget{
  175. Type: targetType,
  176. }
  177. // Add resolver information from upstream context
  178. if ctx != nil && ctx.Resolver != "" {
  179. target.Resolver = ctx.Resolver
  180. }
  181. // Check if the address contains Nginx variables - skip if it does
  182. if strings.Contains(addr, "$") {
  183. return ProxyTarget{}
  184. }
  185. // Check for consul service discovery patterns
  186. if isConsulServiceDiscovery(serverAddr) {
  187. target.IsConsul = true
  188. target.ServiceURL = serverAddr
  189. // Extract consul DNS host (e.g., "service.consul")
  190. if strings.Contains(addr, "service.consul") {
  191. target.Host = "service.consul"
  192. // For consul service discovery, we use a placeholder port since the actual port is dynamic
  193. target.Port = "dynamic"
  194. } else {
  195. // Fallback to regular parsing
  196. parsed := parseAddressOnly(addr)
  197. target.Host = parsed.Host
  198. target.Port = parsed.Port
  199. }
  200. return target
  201. }
  202. // Regular address parsing
  203. parsed := parseAddressOnly(addr)
  204. target.Host = parsed.Host
  205. target.Port = parsed.Port
  206. // Skip if this is the HTTP challenge port used by Let's Encrypt
  207. if target.Host == "127.0.0.1" && target.Port == settings.CertSettings.HTTPChallengePort {
  208. return ProxyTarget{}
  209. }
  210. return target
  211. }
  212. // isConsulServiceDiscovery checks if the server address is a consul service discovery configuration
  213. func isConsulServiceDiscovery(serverAddr string) bool {
  214. return strings.Contains(serverAddr, "service.consul") &&
  215. (strings.Contains(serverAddr, "service=") || strings.Contains(serverAddr, "resolve"))
  216. }
  217. // parseAddressOnly parses just the address portion without consul-specific logic
  218. func parseAddressOnly(addr string) ProxyTarget {
  219. // Handle IPv6 addresses
  220. if strings.HasPrefix(addr, "[") {
  221. // IPv6 format: [::1]:8080
  222. if idx := strings.LastIndex(addr, "]:"); idx != -1 {
  223. host := addr[1:idx]
  224. port := addr[idx+2:]
  225. return ProxyTarget{
  226. Host: host,
  227. Port: port,
  228. }
  229. }
  230. // IPv6 without port: [::1]
  231. host := strings.Trim(addr, "[]")
  232. return ProxyTarget{
  233. Host: host,
  234. Port: "80",
  235. }
  236. }
  237. // Handle IPv4 addresses and hostnames
  238. if strings.Contains(addr, ":") {
  239. parts := strings.Split(addr, ":")
  240. if len(parts) == 2 {
  241. return ProxyTarget{
  242. Host: parts[0],
  243. Port: parts[1],
  244. }
  245. }
  246. }
  247. // No port specified, use default
  248. return ProxyTarget{
  249. Host: addr,
  250. Port: "80",
  251. }
  252. }
  253. // deduplicateTargets removes duplicate proxy targets
  254. func deduplicateTargets(targets []ProxyTarget) []ProxyTarget {
  255. seen := make(map[string]bool)
  256. var result []ProxyTarget
  257. for _, target := range targets {
  258. // Create a unique key that includes resolver and consul information
  259. key := target.Host + ":" + target.Port + ":" + target.Type + ":" + target.Resolver
  260. if target.IsConsul {
  261. key += ":consul:" + target.ServiceURL
  262. }
  263. if !seen[key] {
  264. seen[key] = true
  265. result = append(result, target)
  266. }
  267. }
  268. return result
  269. }
  270. // isUpstreamReference checks if a proxy_pass URL references an upstream block
  271. func isUpstreamReference(proxyPass string, upstreamNames map[string]bool) bool {
  272. proxyPass = strings.TrimSpace(proxyPass)
  273. // For HTTP/HTTPS URLs, parse the URL to extract the hostname
  274. if strings.HasPrefix(proxyPass, "http://") || strings.HasPrefix(proxyPass, "https://") {
  275. // Handle URLs with nginx variables (e.g., "https://myUpStr$request_uri")
  276. // Extract the scheme and hostname part before any nginx variables
  277. schemeAndHost := proxyPass
  278. if dollarIndex := strings.Index(proxyPass, "$"); dollarIndex != -1 {
  279. schemeAndHost = proxyPass[:dollarIndex]
  280. }
  281. // Try to parse the URL, if it fails, try manual extraction
  282. if parsedURL, err := url.Parse(schemeAndHost); err == nil {
  283. hostname := parsedURL.Hostname()
  284. // Check if the hostname matches any upstream name
  285. return upstreamNames[hostname]
  286. } else {
  287. // Fallback: manually extract hostname for URLs with variables
  288. // Remove scheme prefix
  289. withoutScheme := proxyPass
  290. if strings.HasPrefix(proxyPass, "https://") {
  291. withoutScheme = strings.TrimPrefix(proxyPass, "https://")
  292. } else if strings.HasPrefix(proxyPass, "http://") {
  293. withoutScheme = strings.TrimPrefix(proxyPass, "http://")
  294. }
  295. // Extract hostname before any path, port, or variable
  296. hostname := withoutScheme
  297. if slashIndex := strings.Index(hostname, "/"); slashIndex != -1 {
  298. hostname = hostname[:slashIndex]
  299. }
  300. if colonIndex := strings.Index(hostname, ":"); colonIndex != -1 {
  301. hostname = hostname[:colonIndex]
  302. }
  303. if dollarIndex := strings.Index(hostname, "$"); dollarIndex != -1 {
  304. hostname = hostname[:dollarIndex]
  305. }
  306. return upstreamNames[hostname]
  307. }
  308. }
  309. // For stream module, proxy_pass can directly reference upstream name without protocol
  310. // Check if the proxy_pass value directly matches an upstream name
  311. if !strings.Contains(proxyPass, "://") && !strings.Contains(proxyPass, ":") {
  312. return upstreamNames[proxyPass]
  313. }
  314. return false
  315. }