checker.go 22 KB

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