upstream_parser.go 12 KB

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