checker.go 18 KB

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