checker.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768
  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 sites for checking (enabled sites only)", 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. // GetSitesList returns sites as a slice
  434. func (sc *SiteChecker) GetSitesList() []*SiteInfo {
  435. sc.mu.RLock()
  436. defer sc.mu.RUnlock()
  437. result := make([]*SiteInfo, 0, len(sc.sites))
  438. for _, site := range sc.sites {
  439. result = append(result, site)
  440. }
  441. return result
  442. }
  443. // extractDomainName extracts domain name from URL
  444. func extractDomainName(siteURL string) string {
  445. parsed, err := url.Parse(siteURL)
  446. if err != nil {
  447. return siteURL
  448. }
  449. return parsed.Host
  450. }
  451. // extractTitle extracts title from HTML content
  452. func extractTitle(html string) string {
  453. titleRegex := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
  454. matches := titleRegex.FindStringSubmatch(html)
  455. if len(matches) > 1 {
  456. return strings.TrimSpace(matches[1])
  457. }
  458. return ""
  459. }
  460. // extractFavicon extracts favicon URL and data from HTML
  461. func (sc *SiteChecker) extractFavicon(ctx context.Context, siteURL, html string) (string, string) {
  462. parsedURL, err := url.Parse(siteURL)
  463. if err != nil {
  464. return "", ""
  465. }
  466. // Look for favicon link in HTML
  467. faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["']`)
  468. matches := faviconRegex.FindStringSubmatch(html)
  469. var faviconURL string
  470. if len(matches) > 1 {
  471. faviconURL = matches[1]
  472. } else {
  473. // Default favicon location
  474. faviconURL = "/favicon.ico"
  475. }
  476. // Convert relative URL to absolute
  477. if !strings.HasPrefix(faviconURL, "http") {
  478. baseURL := fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host)
  479. if strings.HasPrefix(faviconURL, "/") {
  480. faviconURL = baseURL + faviconURL
  481. } else {
  482. faviconURL = baseURL + "/" + faviconURL
  483. }
  484. }
  485. // Download favicon
  486. faviconData := sc.downloadFavicon(ctx, faviconURL)
  487. return faviconURL, faviconData
  488. }
  489. // downloadFavicon downloads and encodes favicon as base64
  490. func (sc *SiteChecker) downloadFavicon(ctx context.Context, faviconURL string) string {
  491. req, err := http.NewRequestWithContext(ctx, "GET", faviconURL, nil)
  492. if err != nil {
  493. return ""
  494. }
  495. req.Header.Set("User-Agent", sc.options.UserAgent)
  496. resp, err := sc.client.Do(req)
  497. if err != nil {
  498. return ""
  499. }
  500. defer resp.Body.Close()
  501. if resp.StatusCode != http.StatusOK {
  502. return ""
  503. }
  504. // Limit favicon size to 1MB
  505. body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
  506. if err != nil {
  507. return ""
  508. }
  509. // Get content type
  510. contentType := resp.Header.Get("Content-Type")
  511. if contentType == "" {
  512. // Try to determine from URL extension
  513. if strings.HasSuffix(faviconURL, ".png") {
  514. contentType = "image/png"
  515. } else if strings.HasSuffix(faviconURL, ".ico") {
  516. contentType = "image/x-icon"
  517. } else {
  518. contentType = "image/x-icon" // default
  519. }
  520. }
  521. // Encode as data URL
  522. encoded := base64.StdEncoding.EncodeToString(body)
  523. return fmt.Sprintf("data:%s;base64,%s", contentType, encoded)
  524. }
  525. // generateDisplayURL generates the URL to display in UI based on health check protocol
  526. func generateDisplayURL(originalURL, protocol string) string {
  527. parsed, err := url.Parse(originalURL)
  528. if err != nil {
  529. logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
  530. return originalURL
  531. }
  532. logger.Debugf("Generating display URL for %s with protocol %s", originalURL, protocol)
  533. // Determine the optimal scheme (prefer HTTPS if available)
  534. scheme := determineOptimalScheme(parsed, protocol)
  535. hostname := parsed.Hostname()
  536. port := parsed.Port()
  537. // For HTTP/HTTPS, return clean URL without default ports
  538. if scheme == "http" || scheme == "https" {
  539. // Build URL without default ports
  540. var result string
  541. if port == "" || (port == "80" && scheme == "http") || (port == "443" && scheme == "https") {
  542. // No port or default port - don't show port
  543. result = fmt.Sprintf("%s://%s", scheme, hostname)
  544. } else {
  545. // Non-default port - show port
  546. result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
  547. }
  548. logger.Debugf("HTTP/HTTPS display URL: %s", result)
  549. return result
  550. }
  551. // For gRPC/gRPCS, show the connection address format without default ports
  552. if scheme == "grpc" || scheme == "grpcs" {
  553. if port == "" {
  554. // Determine default port based on scheme
  555. if scheme == "grpcs" {
  556. port = "443"
  557. } else {
  558. port = "80"
  559. }
  560. }
  561. // Don't show default ports for gRPC either
  562. var result string
  563. if (port == "80" && scheme == "grpc") || (port == "443" && scheme == "grpcs") {
  564. result = fmt.Sprintf("%s://%s", scheme, hostname)
  565. } else {
  566. result = fmt.Sprintf("%s://%s:%s", scheme, hostname, port)
  567. }
  568. logger.Debugf("gRPC/gRPCS display URL: %s", result)
  569. return result
  570. }
  571. // Fallback to original URL
  572. logger.Debugf("Using fallback display URL: %s", originalURL)
  573. return originalURL
  574. }
  575. // isGRPCProtocol checks if the protocol is gRPC-based
  576. func isGRPCProtocol(protocol string) bool {
  577. return protocol == "grpc" || protocol == "grpcs"
  578. }
  579. // parseURLComponents extracts scheme and host:port from URL based on health check protocol
  580. func parseURLComponents(originalURL, healthCheckProtocol string) (scheme, hostPort string) {
  581. parsed, err := url.Parse(originalURL)
  582. if err != nil {
  583. logger.Debugf("Failed to parse URL %s: %v", originalURL, err)
  584. return healthCheckProtocol, originalURL
  585. }
  586. // Determine the best scheme to use
  587. scheme = determineOptimalScheme(parsed, healthCheckProtocol)
  588. // Extract hostname and port
  589. hostname := parsed.Hostname()
  590. if hostname == "" {
  591. // Fallback to original URL if we can't parse hostname
  592. return scheme, originalURL
  593. }
  594. port := parsed.Port()
  595. if port == "" {
  596. // Use default port based on scheme, but don't include it in hostPort for default ports
  597. switch scheme {
  598. case "https", "grpcs":
  599. // Default HTTPS port 443 - don't show in hostPort
  600. hostPort = hostname
  601. case "http", "grpc":
  602. // Default HTTP port 80 - don't show in hostPort
  603. hostPort = hostname
  604. default:
  605. hostPort = hostname
  606. }
  607. } else {
  608. // Non-default port specified
  609. isDefaultPort := (port == "80" && (scheme == "http" || scheme == "grpc")) ||
  610. (port == "443" && (scheme == "https" || scheme == "grpcs"))
  611. if isDefaultPort {
  612. // Don't show default ports
  613. hostPort = hostname
  614. } else {
  615. // Show non-default ports
  616. hostPort = hostname + ":" + port
  617. }
  618. }
  619. return scheme, hostPort
  620. }
  621. // determineOptimalScheme determines the best scheme to use based on original URL and health check protocol
  622. func determineOptimalScheme(parsed *url.URL, healthCheckProtocol string) string {
  623. // If health check protocol is specified, use it, but with special handling for HTTP/HTTPS
  624. if healthCheckProtocol != "" {
  625. // Special case: Don't downgrade HTTPS to HTTP
  626. if healthCheckProtocol == "http" && parsed.Scheme == "https" {
  627. // logger.Debugf("Preserving HTTPS scheme instead of downgrading to HTTP")
  628. return "https"
  629. }
  630. // For gRPC protocols, always use the specified protocol
  631. if healthCheckProtocol == "grpc" || healthCheckProtocol == "grpcs" {
  632. return healthCheckProtocol
  633. }
  634. // For HTTPS health check protocol, always use HTTPS
  635. if healthCheckProtocol == "https" {
  636. return "https"
  637. }
  638. // For HTTP health check protocol, only use HTTP if original was also HTTP
  639. if healthCheckProtocol == "http" && parsed.Scheme == "http" {
  640. return "http"
  641. }
  642. }
  643. // If no health check protocol, or if we need to fall back, prefer HTTPS if the original URL is HTTPS
  644. if parsed.Scheme == "https" {
  645. return "https"
  646. }
  647. // Default to HTTP
  648. return "http"
  649. }