diff --git a/backend/internal/caddy/importer.go b/backend/internal/caddy/importer.go index 768ef7bd..b05cbccd 100644 --- a/backend/internal/caddy/importer.go +++ b/backend/internal/caddy/importer.go @@ -42,6 +42,7 @@ type CaddyHTTP struct { // CaddyServer represents a single server configuration. type CaddyServer struct { + Listen []string `json:"listen,omitempty"` Routes []*CaddyRoute `json:"routes,omitempty"` TLSConnectionPolicies interface{} `json:"tls_connection_policies,omitempty"` } @@ -173,6 +174,16 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) { seenDomains := make(map[string]bool) for serverName, server := range config.Apps.HTTP.Servers { + // Detect if this server uses SSL based on listen address or TLS policies + serverUsesSSL := server.TLSConnectionPolicies != nil + for _, listenAddr := range server.Listen { + // Check if listening on :443 or any HTTPS port indicator + if strings.Contains(listenAddr, ":443") || strings.HasSuffix(listenAddr, "443") { + serverUsesSSL = true + break + } + } + for routeIdx, route := range server.Routes { for _, match := range route.Match { for _, hostMatcher := range match.Host { @@ -188,7 +199,7 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) { // Extract reverse proxy handler host := ParsedHost{ DomainNames: domain, - SSLForced: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil, + SSLForced: strings.HasPrefix(domain, "https") || serverUsesSSL, } // Find reverse_proxy handler (may be nested in subroute) diff --git a/backend/internal/caddy/importer_test.go b/backend/internal/caddy/importer_test.go index f26b5fce..ee8ebdaf 100644 --- a/backend/internal/caddy/importer_test.go +++ b/backend/internal/caddy/importer_test.go @@ -166,6 +166,35 @@ func TestImporter_ExtractHosts(t *testing.T) { assert.Len(t, result.Hosts[0].Warnings, 2) assert.Contains(t, result.Hosts[0].Warnings, "File server directives not supported") assert.Contains(t, result.Hosts[0].Warnings, "Rewrite rules not supported - manual configuration required") + + // Test Case 6: SSL Detection via Listen Address (:443) + sslViaListenJSON := []byte(`{ + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [":443"], + "routes": [ + { + "match": [{"host": ["secure.example.com"]}], + "handle": [ + { + "handler": "reverse_proxy", + "upstreams": [{"dial": "127.0.0.1:9000"}] + } + ] + } + ] + } + } + } + } + }`) + result, err = importer.ExtractHosts(sslViaListenJSON) + assert.NoError(t, err) + assert.Len(t, result.Hosts, 1) + assert.Equal(t, "secure.example.com", result.Hosts[0].DomainNames) + assert.True(t, result.Hosts[0].SSLForced, "SSLForced should be true when server listens on :443") } func TestImporter_ImportFile(t *testing.T) { diff --git a/frontend/src/pages/ProxyHosts.tsx b/frontend/src/pages/ProxyHosts.tsx index bcaa6c8e..4cd93c06 100644 --- a/frontend/src/pages/ProxyHosts.tsx +++ b/frontend/src/pages/ProxyHosts.tsx @@ -22,10 +22,18 @@ export default function ProxyHosts() { const linkBehavior = settings?.['ui.domain_link_behavior'] || 'new_tab' // Create a map of domain -> certificate status for quick lookup + // Handles both single domains and comma-separated multi-domain certs const certStatusByDomain = useMemo(() => { const map: Record = {} certificates.forEach(cert => { - map[cert.domain] = { status: cert.status, provider: cert.provider } + // Handle comma-separated domains (SANs) + const domains = cert.domain.split(',').map(d => d.trim().toLowerCase()) + domains.forEach(domain => { + // Only set if not already set (first cert wins) + if (!map[domain]) { + map[domain] = { status: cert.status, provider: cert.provider } + } + }) }) return map }, [certificates]) @@ -148,11 +156,12 @@ export default function ProxyHosts() { {(() => { - // Get the primary domain to look up cert status - const primaryDomain = host.domain_names.split(',')[0]?.trim() + // Get the primary domain to look up cert status (case-insensitive) + const primaryDomain = host.domain_names.split(',')[0]?.trim().toLowerCase() const certInfo = certStatusByDomain[primaryDomain] const isUntrusted = certInfo?.status === 'untrusted' const isStaging = certInfo?.provider?.includes('staging') + const hasCertInfo = !!certInfo return (
@@ -185,6 +194,10 @@ export default function ProxyHosts() {
⚠️ Staging cert - browsers won't trust
+ ) : hasCertInfo ? ( +
+ Let's Encrypt ✓ +
) : (
Let's Encrypt (Auto)