checker.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. package sitecheck
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "encoding/base64"
  6. "fmt"
  7. "io"
  8. "maps"
  9. "net"
  10. "net/http"
  11. "net/url"
  12. "regexp"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/0xJacky/Nginx-UI/internal/site"
  17. "github.com/0xJacky/Nginx-UI/model"
  18. "github.com/0xJacky/Nginx-UI/query"
  19. "github.com/uozi-tech/cosy/logger"
  20. )
  21. // Site config cache with expiration
  22. var (
  23. siteConfigCache = make(map[string]*siteConfigCacheEntry)
  24. siteConfigMutex sync.RWMutex
  25. cacheExpiry = 5 * time.Minute // Cache entries expire after 5 minutes
  26. lastBatchLoad time.Time
  27. )
  28. type siteConfigCacheEntry struct {
  29. config *model.SiteConfig
  30. expiresAt time.Time
  31. }
  32. type SiteChecker struct {
  33. sites map[string]*SiteInfo
  34. mu sync.RWMutex
  35. options CheckOptions
  36. client *http.Client
  37. onUpdateCallback func([]*SiteInfo) // Callback for notifying updates
  38. }
  39. // NewSiteChecker creates a new site checker
  40. func NewSiteChecker(options CheckOptions) *SiteChecker {
  41. transport := &http.Transport{
  42. Dial: (&net.Dialer{
  43. Timeout: 5 * time.Second,
  44. }).Dial,
  45. TLSHandshakeTimeout: 5 * time.Second,
  46. TLSClientConfig: &tls.Config{
  47. InsecureSkipVerify: true, // Skip SSL verification for internal sites
  48. },
  49. }
  50. client := &http.Client{
  51. Transport: transport,
  52. Timeout: options.Timeout,
  53. }
  54. if !options.FollowRedirects {
  55. client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  56. return http.ErrUseLastResponse
  57. }
  58. } else if options.MaxRedirects > 0 {
  59. client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
  60. if len(via) >= options.MaxRedirects {
  61. return fmt.Errorf("stopped after %d redirects", options.MaxRedirects)
  62. }
  63. return nil
  64. }
  65. }
  66. return &SiteChecker{
  67. sites: make(map[string]*SiteInfo),
  68. options: options,
  69. client: client,
  70. }
  71. }
  72. // SetUpdateCallback sets the callback function for site updates
  73. func (sc *SiteChecker) SetUpdateCallback(callback func([]*SiteInfo)) {
  74. sc.onUpdateCallback = callback
  75. }
  76. // CollectSites collects URLs from enabled indexed sites only
  77. func (sc *SiteChecker) CollectSites() {
  78. sc.mu.Lock()
  79. defer sc.mu.Unlock()
  80. // Clear existing sites
  81. sc.sites = make(map[string]*SiteInfo)
  82. // Debug: log indexed sites count
  83. logger.Infof("Found %d indexed sites", len(site.IndexedSites))
  84. // Collect URLs from indexed sites, but only from enabled sites
  85. for siteName, indexedSite := range site.IndexedSites {
  86. // Check site status - only collect from enabled sites
  87. siteStatus := site.GetSiteStatus(siteName)
  88. if siteStatus != site.StatusEnabled {
  89. // logger.Debugf("Skipping site %s (status: %s) - only collecting from enabled sites", siteName, siteStatus)
  90. continue
  91. }
  92. // logger.Debugf("Processing enabled site: %s with %d URLs", siteName, len(indexedSite.Urls))
  93. for _, url := range indexedSite.Urls {
  94. if url != "" {
  95. // logger.Debugf("Adding site URL: %s", url)
  96. // Load site config to determine display URL
  97. config, err := LoadSiteConfig(url)
  98. protocol := "http" // default protocol
  99. if err == nil && config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
  100. protocol = config.HealthCheckConfig.Protocol
  101. // logger.Debugf("Site %s using protocol: %s", url, protocol)
  102. } else {
  103. logger.Debugf("Site %s using default protocol: %s (config error: %v)", url, protocol, err)
  104. }
  105. // Parse URL components for legacy fields
  106. _, hostPort := parseURLComponents(url, protocol)
  107. // Get or create site config to get ID
  108. siteConfig := getOrCreateSiteConfigForURL(url)
  109. siteInfo := &SiteInfo{
  110. ID: siteConfig.ID,
  111. Host: siteConfig.Host,
  112. Port: siteConfig.Port,
  113. Scheme: siteConfig.Scheme,
  114. DisplayURL: siteConfig.GetURL(),
  115. Name: extractDomainName(url),
  116. Status: StatusChecking,
  117. LastChecked: time.Now().Unix(),
  118. // Legacy fields for backward compatibility
  119. URL: url,
  120. HealthCheckProtocol: protocol,
  121. HostPort: hostPort,
  122. }
  123. sc.sites[url] = siteInfo
  124. }
  125. }
  126. }
  127. logger.Infof("Collected %d enabled sites for checking", len(sc.sites))
  128. }
  129. // loadAllSiteConfigs loads all site configs from database and caches them
  130. func loadAllSiteConfigs() error {
  131. siteConfigMutex.Lock()
  132. defer siteConfigMutex.Unlock()
  133. // Skip database operation if query.SiteConfig is nil (e.g., in tests)
  134. if query.SiteConfig == nil {
  135. logger.Debugf("Skipping site config batch load - query.SiteConfig is nil (likely in test environment)")
  136. lastBatchLoad = time.Now()
  137. return nil
  138. }
  139. sc := query.SiteConfig
  140. configs, err := sc.Find()
  141. if err != nil {
  142. return fmt.Errorf("failed to load site configs: %w", err)
  143. }
  144. now := time.Now()
  145. expiry := now.Add(cacheExpiry)
  146. // Clear existing cache
  147. siteConfigCache = make(map[string]*siteConfigCacheEntry)
  148. // Cache all configs
  149. for _, config := range configs {
  150. siteConfigCache[config.Host] = &siteConfigCacheEntry{
  151. config: config,
  152. expiresAt: expiry,
  153. }
  154. }
  155. lastBatchLoad = now
  156. logger.Debugf("Loaded %d site configs into cache", len(configs))
  157. return nil
  158. }
  159. // getCachedSiteConfig gets a site config from cache, loading all configs if needed
  160. func getCachedSiteConfig(host string) (*model.SiteConfig, bool) {
  161. siteConfigMutex.RLock()
  162. // Check if we need to refresh the cache
  163. needsRefresh := time.Since(lastBatchLoad) > cacheExpiry
  164. if needsRefresh {
  165. siteConfigMutex.RUnlock()
  166. // Reload all configs if cache is expired
  167. if err := loadAllSiteConfigs(); err != nil {
  168. logger.Errorf("Failed to reload site configs: %v", err)
  169. return nil, false
  170. }
  171. siteConfigMutex.RLock()
  172. }
  173. entry, exists := siteConfigCache[host]
  174. siteConfigMutex.RUnlock()
  175. if !exists || time.Now().After(entry.expiresAt) {
  176. return nil, false
  177. }
  178. return entry.config, true
  179. }
  180. // setCachedSiteConfig sets a site config in cache
  181. func setCachedSiteConfig(host string, config *model.SiteConfig) {
  182. siteConfigMutex.Lock()
  183. defer siteConfigMutex.Unlock()
  184. siteConfigCache[host] = &siteConfigCacheEntry{
  185. config: config,
  186. expiresAt: time.Now().Add(cacheExpiry),
  187. }
  188. }
  189. // InvalidateSiteConfigCache invalidates the entire site config cache
  190. func InvalidateSiteConfigCache() {
  191. siteConfigMutex.Lock()
  192. defer siteConfigMutex.Unlock()
  193. siteConfigCache = make(map[string]*siteConfigCacheEntry)
  194. lastBatchLoad = time.Time{} // Reset batch load time to force reload
  195. logger.Debugf("Site config cache invalidated")
  196. }
  197. // InvalidateSiteConfigCacheForHost invalidates cache for a specific host
  198. func InvalidateSiteConfigCacheForHost(host string) {
  199. siteConfigMutex.Lock()
  200. defer siteConfigMutex.Unlock()
  201. delete(siteConfigCache, host)
  202. logger.Debugf("Site config cache invalidated for host: %s", host)
  203. }
  204. // getOrCreateSiteConfigForURL gets or creates a site config for the given URL
  205. func getOrCreateSiteConfigForURL(url string) *model.SiteConfig {
  206. // Parse URL to get host:port
  207. tempConfig := &model.SiteConfig{}
  208. tempConfig.SetFromURL(url)
  209. // Try to get from cache first
  210. if config, found := getCachedSiteConfig(tempConfig.Host); found {
  211. return config
  212. }
  213. // Not in cache, query database
  214. sc := query.SiteConfig
  215. siteConfig, err := sc.Where(sc.Host.Eq(tempConfig.Host)).First()
  216. if err != nil {
  217. // Record doesn't exist, create a new one
  218. newConfig := &model.SiteConfig{
  219. Host: tempConfig.Host,
  220. Port: tempConfig.Port,
  221. Scheme: tempConfig.Scheme,
  222. DisplayURL: url,
  223. HealthCheckEnabled: true,
  224. CheckInterval: 300,
  225. Timeout: 10,
  226. UserAgent: "Nginx-UI Site Checker/1.0",
  227. MaxRedirects: 3,
  228. FollowRedirects: true,
  229. CheckFavicon: true,
  230. }
  231. // Create the record in database
  232. if err := sc.Create(newConfig); err != nil {
  233. logger.Errorf("Failed to create site config for %s: %v", url, err)
  234. // Return temp config with a fake ID to avoid crashes
  235. tempConfig.ID = 0
  236. return tempConfig
  237. }
  238. // Cache the new config
  239. setCachedSiteConfig(tempConfig.Host, newConfig)
  240. return newConfig
  241. }
  242. // Record exists, ensure it has the correct URL information
  243. if siteConfig.DisplayURL == "" {
  244. siteConfig.DisplayURL = url
  245. siteConfig.SetFromURL(url)
  246. // Try to save the updated config, but don't fail if it doesn't work
  247. sc.Save(siteConfig)
  248. }
  249. // Cache the config
  250. setCachedSiteConfig(tempConfig.Host, siteConfig)
  251. return siteConfig
  252. }
  253. // CheckSite checks a single site's availability
  254. func (sc *SiteChecker) CheckSite(ctx context.Context, siteURL string) (*SiteInfo, error) {
  255. // Try enhanced health check first if config exists
  256. config, err := LoadSiteConfig(siteURL)
  257. if err == nil && config != nil && config.HealthCheckConfig != nil {
  258. enhancedChecker := NewEnhancedSiteChecker()
  259. siteInfo, err := enhancedChecker.CheckSiteWithConfig(ctx, siteURL, config.HealthCheckConfig)
  260. if err == nil && siteInfo != nil {
  261. // Fill in additional details
  262. siteInfo.Name = extractDomainName(siteURL)
  263. siteInfo.LastChecked = time.Now().Unix()
  264. // Set health check protocol and display URL
  265. siteInfo.HealthCheckProtocol = config.HealthCheckConfig.Protocol
  266. siteInfo.DisplayURL = generateDisplayURL(siteURL, config.HealthCheckConfig.Protocol)
  267. // Parse URL components
  268. scheme, hostPort := parseURLComponents(siteURL, config.HealthCheckConfig.Protocol)
  269. siteInfo.Scheme = scheme
  270. siteInfo.HostPort = hostPort
  271. // Try to get favicon if enabled and not a gRPC check
  272. if sc.options.CheckFavicon && !isGRPCProtocol(config.HealthCheckConfig.Protocol) {
  273. faviconURL, faviconData := sc.tryGetFavicon(ctx, siteURL)
  274. siteInfo.FaviconURL = faviconURL
  275. siteInfo.FaviconData = faviconData
  276. }
  277. return siteInfo, nil
  278. }
  279. }
  280. // Fallback to basic HTTP check, but preserve original protocol if available
  281. originalProtocol := "http" // default
  282. if config != nil && config.HealthCheckConfig != nil && config.HealthCheckConfig.Protocol != "" {
  283. originalProtocol = config.HealthCheckConfig.Protocol
  284. }
  285. return sc.checkSiteBasic(ctx, siteURL, originalProtocol)
  286. }
  287. // checkSiteBasic performs basic HTTP health check
  288. func (sc *SiteChecker) checkSiteBasic(ctx context.Context, siteURL string, originalProtocol string) (*SiteInfo, error) {
  289. start := time.Now()
  290. req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
  291. if err != nil {
  292. return nil, fmt.Errorf("failed to create request: %w", err)
  293. }
  294. req.Header.Set("User-Agent", sc.options.UserAgent)
  295. resp, err := sc.client.Do(req)
  296. if err != nil {
  297. // Parse URL components for legacy fields
  298. _, hostPort := parseURLComponents(siteURL, originalProtocol)
  299. // Get or create site config to get ID
  300. siteConfig := getOrCreateSiteConfigForURL(siteURL)
  301. return &SiteInfo{
  302. ID: siteConfig.ID,
  303. Host: siteConfig.Host,
  304. Port: siteConfig.Port,
  305. Scheme: siteConfig.Scheme,
  306. DisplayURL: siteConfig.GetURL(),
  307. Name: extractDomainName(siteURL),
  308. Status: StatusOffline,
  309. ResponseTime: time.Since(start).Milliseconds(),
  310. LastChecked: time.Now().Unix(),
  311. Error: err.Error(),
  312. // Legacy fields for backward compatibility
  313. URL: siteURL,
  314. HealthCheckProtocol: originalProtocol,
  315. HostPort: hostPort,
  316. }, nil
  317. }
  318. defer resp.Body.Close()
  319. responseTime := time.Since(start).Milliseconds()
  320. // Parse URL components for legacy fields
  321. _, hostPort := parseURLComponents(siteURL, originalProtocol)
  322. // Get or create site config to get ID
  323. siteConfig := getOrCreateSiteConfigForURL(siteURL)
  324. siteInfo := &SiteInfo{
  325. ID: siteConfig.ID,
  326. Host: siteConfig.Host,
  327. Port: siteConfig.Port,
  328. Scheme: siteConfig.Scheme,
  329. DisplayURL: siteConfig.GetURL(),
  330. Name: extractDomainName(siteURL),
  331. StatusCode: resp.StatusCode,
  332. ResponseTime: responseTime,
  333. LastChecked: time.Now().Unix(),
  334. // Legacy fields for backward compatibility
  335. URL: siteURL,
  336. HealthCheckProtocol: originalProtocol,
  337. HostPort: hostPort,
  338. }
  339. // Determine status based on status code
  340. if resp.StatusCode >= 200 && resp.StatusCode < 400 {
  341. siteInfo.Status = StatusOnline
  342. } else {
  343. siteInfo.Status = StatusError
  344. siteInfo.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
  345. }
  346. // Read response body for title and favicon extraction
  347. body, err := io.ReadAll(resp.Body)
  348. if err != nil {
  349. logger.Warnf("Failed to read response body for %s: %v", siteURL, err)
  350. return siteInfo, nil
  351. }
  352. // Extract title
  353. siteInfo.Title = extractTitle(string(body))
  354. // Extract favicon if enabled
  355. if sc.options.CheckFavicon {
  356. faviconURL, faviconData := sc.extractFavicon(ctx, siteURL, string(body))
  357. siteInfo.FaviconURL = faviconURL
  358. siteInfo.FaviconData = faviconData
  359. }
  360. return siteInfo, nil
  361. }
  362. // tryGetFavicon attempts to get favicon for enhanced checks
  363. func (sc *SiteChecker) tryGetFavicon(ctx context.Context, siteURL string) (string, string) {
  364. // Make a simple GET request to get the HTML
  365. req, err := http.NewRequestWithContext(ctx, "GET", siteURL, nil)
  366. if err != nil {
  367. return "", ""
  368. }
  369. req.Header.Set("User-Agent", sc.options.UserAgent)
  370. resp, err := sc.client.Do(req)
  371. if err != nil {
  372. return "", ""
  373. }
  374. defer resp.Body.Close()
  375. if resp.StatusCode < 200 || resp.StatusCode >= 400 {
  376. return "", ""
  377. }
  378. body, err := io.ReadAll(resp.Body)
  379. if err != nil {
  380. return "", ""
  381. }
  382. return sc.extractFavicon(ctx, siteURL, string(body))
  383. }
  384. // CheckAllSites checks all collected sites concurrently
  385. func (sc *SiteChecker) CheckAllSites(ctx context.Context) {
  386. sc.mu.RLock()
  387. urls := make([]string, 0, len(sc.sites))
  388. for url := range sc.sites {
  389. urls = append(urls, url)
  390. }
  391. sc.mu.RUnlock()
  392. // Use a semaphore to limit concurrent requests
  393. semaphore := make(chan struct{}, 10) // Max 10 concurrent requests
  394. var wg sync.WaitGroup
  395. for _, url := range urls {
  396. wg.Add(1)
  397. go func(siteURL string) {
  398. defer wg.Done()
  399. semaphore <- struct{}{} // Acquire semaphore
  400. defer func() { <-semaphore }() // Release semaphore
  401. siteInfo, err := sc.CheckSite(ctx, siteURL)
  402. if err != nil {
  403. logger.Errorf("Failed to check site %s: %v", siteURL, err)
  404. return
  405. }
  406. sc.mu.Lock()
  407. sc.sites[siteURL] = siteInfo
  408. sc.mu.Unlock()
  409. }(url)
  410. }
  411. wg.Wait()
  412. // logger.Infof("Completed checking %d sites", len(urls))
  413. // Notify WebSocket clients of the update
  414. if sc.onUpdateCallback != nil {
  415. sites := make([]*SiteInfo, 0, len(sc.sites))
  416. sc.mu.RLock()
  417. for _, site := range sc.sites {
  418. sites = append(sites, site)
  419. }
  420. sc.mu.RUnlock()
  421. sc.onUpdateCallback(sites)
  422. }
  423. }
  424. // GetSites returns all checked sites
  425. func (sc *SiteChecker) GetSites() map[string]*SiteInfo {
  426. sc.mu.RLock()
  427. defer sc.mu.RUnlock()
  428. // Create a copy to avoid race conditions
  429. result := make(map[string]*SiteInfo)
  430. maps.Copy(result, sc.sites)
  431. return result
  432. }
  433. // GetSiteCount returns the number of sites being monitored
  434. func (sc *SiteChecker) GetSiteCount() int {
  435. sc.mu.RLock()
  436. defer sc.mu.RUnlock()
  437. return len(sc.sites)
  438. }
  439. // GetSitesList returns sites as a slice
  440. func (sc *SiteChecker) GetSitesList() []*SiteInfo {
  441. sc.mu.RLock()
  442. defer sc.mu.RUnlock()
  443. result := make([]*SiteInfo, 0, len(sc.sites))
  444. for _, site := range sc.sites {
  445. result = append(result, site)
  446. }
  447. return result
  448. }
  449. // extractDomainName extracts domain name from URL
  450. func extractDomainName(siteURL string) string {
  451. parsed, err := url.Parse(siteURL)
  452. if err != nil {
  453. return siteURL
  454. }
  455. return parsed.Host
  456. }
  457. // extractTitle extracts title from HTML content
  458. func extractTitle(html string) string {
  459. titleRegex := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
  460. matches := titleRegex.FindStringSubmatch(html)
  461. if len(matches) > 1 {
  462. return strings.TrimSpace(matches[1])
  463. }
  464. return ""
  465. }
  466. // extractFavicon extracts favicon URL and data from HTML
  467. func (sc *SiteChecker) extractFavicon(ctx context.Context, siteURL, html string) (string, string) {
  468. parsedURL, err := url.Parse(siteURL)
  469. if err != nil {
  470. return "", ""
  471. }
  472. // Look for favicon link in HTML
  473. faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["']`)
  474. matches := faviconRegex.FindStringSubmatch(html)
  475. var faviconURL string
  476. if len(matches) > 1 {
  477. faviconURL = matches[1]
  478. } else {
  479. // Default favicon location
  480. faviconURL = "/favicon.ico"
  481. }
  482. // Convert relative URL to absolute
  483. if !strings.HasPrefix(faviconURL, "http") {
  484. baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
  485. if strings.HasPrefix(faviconURL, "/") {
  486. faviconURL = baseURL + faviconURL
  487. } else {
  488. faviconURL = baseURL + "/" + faviconURL
  489. }
  490. }
  491. // Download favicon
  492. faviconData := sc.downloadFavicon(ctx, faviconURL)
  493. return faviconURL, faviconData
  494. }
  495. // downloadFavicon downloads and encodes favicon as base64
  496. func (sc *SiteChecker) downloadFavicon(ctx context.Context, faviconURL string) string {
  497. req, err := http.NewRequestWithContext(ctx, "GET", faviconURL, nil)
  498. if err != nil {
  499. return ""
  500. }
  501. req.Header.Set("User-Agent", sc.options.UserAgent)
  502. resp, err := sc.client.Do(req)
  503. if err != nil {
  504. return ""
  505. }
  506. defer resp.Body.Close()
  507. if resp.StatusCode != http.StatusOK {
  508. return ""
  509. }
  510. // Limit favicon size to 1MB
  511. body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
  512. if err != nil {
  513. return ""
  514. }
  515. // Get content type
  516. contentType := resp.Header.Get("Content-Type")
  517. if contentType == "" {
  518. // Try to determine from URL extension
  519. if strings.HasSuffix(faviconURL, ".png") {
  520. contentType = "image/png"
  521. } else if strings.HasSuffix(faviconURL, ".ico") {
  522. contentType = "image/x-icon"
  523. } else {
  524. contentType = "image/x-icon" // default
  525. }
  526. }
  527. // Encode as data URL
  528. encoded := base64.StdEncoding.EncodeToString(body)
  529. return fmt.Sprintf("data:%s;base64,%s", contentType, encoded)
  530. }
  531. // generateDisplayURL generates the URL to display in UI based on health check protocol
  532. func generateDisplayURL(originalURL, protocol string) string {
  533. parsed, err := url.Parse(originalURL)
  534. if err != nil {
  535. logger.Errorf("Failed to parse URL %s: %v", originalURL, err)
  536. return originalURL
  537. }
  538. // Determine the optimal scheme (prefer HTTPS if available)
  539. scheme := determineOptimalScheme(parsed, protocol)
  540. hostname := parsed.Hostname()
  541. port := parsed.Port()
  542. // For HTTP/HTTPS, return clean URL without default ports
  543. if scheme == "http" || scheme == "https" {
  544. // Build URL without default ports
  545. var result string
  546. if port == "" || (port == "80" && scheme == "http") || (port == "443" && scheme == "https") {
  547. // No port or default port - don't show port
  548. result = fmt.Sprintf("%s://%s", scheme, hostname)
  549. } else {
  550. // Non-default port - show port
  551. result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
  552. }
  553. return result
  554. }
  555. // For gRPC/gRPCS, show the connection address format without default ports
  556. if scheme == "grpc" || scheme == "grpcs" {
  557. if port == "" {
  558. // Determine default port based on scheme
  559. if scheme == "grpcs" {
  560. port = "443"
  561. } else {
  562. port = "80"
  563. }
  564. }
  565. // Don't show default ports for gRPC either
  566. var result string
  567. if (port == "80" && scheme == "grpc") || (port == "443" && scheme == "grpcs") {
  568. result = fmt.Sprintf("%s://%s", scheme, hostname)
  569. } else {
  570. result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
  571. }
  572. return result
  573. }
  574. // Fallback to original URL
  575. return originalURL
  576. }
  577. // isGRPCProtocol checks if the protocol is gRPC-based
  578. func isGRPCProtocol(protocol string) bool {
  579. return protocol == "grpc" || protocol == "grpcs"
  580. }
  581. // parseURLComponents extracts scheme and host:port from URL based on health check protocol
  582. func parseURLComponents(originalURL, healthCheckProtocol string) (scheme, hostPort string) {
  583. parsed, err := url.Parse(originalURL)
  584. if err != nil {
  585. logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
  586. return healthCheckProtocol, originalURL
  587. }
  588. // Determine the best scheme to use
  589. scheme = determineOptimalScheme(parsed, healthCheckProtocol)
  590. // Extract hostname and port
  591. hostname := parsed.Hostname()
  592. if hostname == "" {
  593. // Fallback to original URL if we can't parse hostname
  594. return scheme, originalURL
  595. }
  596. port := parsed.Port()
  597. if port == "" {
  598. // Use default port based on scheme, but don't include it in hostPort for default ports
  599. switch scheme {
  600. case "https", "grpcs":
  601. // Default HTTPS port 443 - don't show in hostPort
  602. hostPort = hostname
  603. case "http", "grpc":
  604. // Default HTTP port 80 - don't show in hostPort
  605. hostPort = hostname
  606. default:
  607. hostPort = hostname
  608. }
  609. } else {
  610. // Non-default port specified
  611. isDefaultPort := (port == "80" && (scheme == "http" || scheme == "grpc")) ||
  612. (port == "443" && (scheme == "https" || scheme == "grpcs"))
  613. if isDefaultPort {
  614. // Don't show default ports
  615. hostPort = hostname
  616. } else {
  617. // Show non-default ports
  618. hostPort = hostname + ":" + port
  619. }
  620. }
  621. return scheme, hostPort
  622. }
  623. // determineOptimalScheme determines the best scheme to use based on original URL and health check protocol
  624. func determineOptimalScheme(parsed *url.URL, healthCheckProtocol string) string {
  625. // If health check protocol is specified, use it, but with special handling for HTTP/HTTPS
  626. if healthCheckProtocol != "" {
  627. // Special case: Don't downgrade HTTPS to HTTP
  628. if healthCheckProtocol == "http" && parsed.Scheme == "https" {
  629. // logger.Debugf("Preserving HTTPS scheme instead of downgrading to HTTP")
  630. return "https"
  631. }
  632. // For gRPC protocols, always use the specified protocol
  633. if healthCheckProtocol == "grpc" || healthCheckProtocol == "grpcs" {
  634. return healthCheckProtocol
  635. }
  636. // For HTTPS health check protocol, always use HTTPS
  637. if healthCheckProtocol == "https" {
  638. return "https"
  639. }
  640. // For HTTP health check protocol, only use HTTP if original was also HTTP
  641. if healthCheckProtocol == "http" && parsed.Scheme == "http" {
  642. return "http"
  643. }
  644. }
  645. // If no health check protocol, or if we need to fall back, prefer HTTPS if the original URL is HTTPS
  646. if parsed.Scheme == "https" {
  647. return "https"
  648. }
  649. // Default to HTTP
  650. return "http"
  651. }