Skip to content

Commit dac0b0e

Browse files
committed
add www
1 parent cc98672 commit dac0b0e

2 files changed

Lines changed: 228 additions & 35 deletions

File tree

internal/service/site.go

Lines changed: 143 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ func (s SiteService) CreateSite(ctx context.Context, opts CreateSiteOptions) err
273273
vhostDefinition := filepath.Join(vhostDir, "vhost.conf")
274274
serverConfigPath := filepath.Join(s.lswsRoot, "conf", "httpd_config.conf")
275275
siteURL := wordPressBaseURL(opts.Domain, opts.WithLE)
276+
mapDomains := mappedListenerDomains(opts.Domain)
277+
isTopLevelDomain := isTopLevelSiteDomain(opts.Domain)
276278
var wpAccess *wpAdminAccess
277279

278280
s.console.Section("Create site")
@@ -311,7 +313,7 @@ func (s SiteService) CreateSite(ctx context.Context, opts CreateSiteOptions) err
311313
s.console.Bullet("write " + vhostConfig)
312314
s.console.Bullet("write " + vhostDefinition)
313315
s.console.Bullet("append virtualhost block into " + serverConfigPath)
314-
s.console.Bullet("insert listener map in " + serverConfigPath)
316+
s.console.Bullet("insert listener map in " + serverConfigPath + " for hosts: " + strings.Join(mapDomains, ", "))
315317
s.console.Bullet("align ownership to OpenLiteSpeed server user/group for " + siteRoot + " and " + vhostDir)
316318
if opts.OWASPEnabled != nil {
317319
s.console.Bullet("set virtual-host OWASP mod_security: " + enabledLabel(*opts.OWASPEnabled))
@@ -336,7 +338,14 @@ func (s SiteService) CreateSite(ctx context.Context, opts CreateSiteOptions) err
336338
s.console.Bullet("write starter index.php into " + docRoot)
337339
}
338340
if opts.WithLE {
339-
s.console.Bullet("perform domain reachability precheck for Let's Encrypt")
341+
if isTopLevelDomain {
342+
s.console.Bullet("perform domain reachability precheck for " + opts.Domain)
343+
s.console.Bullet("perform optional www reachability precheck for www." + opts.Domain)
344+
s.console.Bullet("if www precheck passes, issue Let's Encrypt cert for both domains")
345+
s.console.Bullet("if www precheck fails, issue Let's Encrypt cert for primary domain only")
346+
} else {
347+
s.console.Bullet("skip domain reachability precheck for subdomain LE issuance")
348+
}
340349
s.console.Bullet("ensure certbot is installed")
341350
s.console.Bullet("issue Let's Encrypt certificate via certbot webroot challenge")
342351
s.console.Bullet("write cert/key into vhost SSL config")
@@ -430,20 +439,33 @@ func (s SiteService) CreateSite(ctx context.Context, opts CreateSiteOptions) err
430439
}
431440

432441
if opts.WithLE {
433-
ok, detail := precheckLEDomainReachability(opts.Domain)
434-
if !ok {
435-
return apperr.New(apperr.CodeValidation, "Let's Encrypt precheck failed: "+detail)
442+
certDomains := []string{opts.Domain}
443+
if isTopLevelDomain {
444+
ok, detail := precheckLEDomainReachability(opts.Domain)
445+
if !ok {
446+
return apperr.New(apperr.CodeValidation, "Let's Encrypt precheck failed for "+opts.Domain+": "+detail)
447+
}
448+
s.console.Success("Let's Encrypt precheck passed for " + opts.Domain + ": " + detail)
449+
450+
wwwDomain := "www." + opts.Domain
451+
okWWW, detailWWW := precheckLEDomainReachability(wwwDomain)
452+
if okWWW {
453+
certDomains = append(certDomains, wwwDomain)
454+
s.console.Success("Let's Encrypt precheck passed for " + wwwDomain + ": " + detailWWW)
455+
} else {
456+
s.console.Warn("Let's Encrypt precheck failed for " + wwwDomain + "; issuing certificate for primary domain only: " + detailWWW)
457+
}
436458
}
437-
s.console.Success("Let's Encrypt precheck passed: domain is reachable over HTTP")
438459

439-
certFile, keyFile, err := s.issueLetsEncryptCertificate(ctx, info, opts.Domain, docRoot)
460+
certFile, keyFile, err := s.issueLetsEncryptCertificate(ctx, info, docRoot, certDomains...)
440461
if err != nil {
441462
return err
442463
}
443464
if err := applyVHostSSLCertificate(vhostConfig, certFile, keyFile); err != nil {
444465
return err
445466
}
446467
s.console.Success("Let's Encrypt certificate issued")
468+
s.console.Bullet("Certificate domains: " + strings.Join(certDomains, ", "))
447469
s.console.Bullet("Certificate: " + certFile)
448470
s.console.Bullet("Private key: " + keyFile)
449471

@@ -1082,6 +1104,13 @@ func (s SiteService) ensureDomainDoesNotExist(domain, vhostDir, serverConfigPath
10821104
return apperr.New(apperr.CodeValidation, fmt.Sprintf("domain %s is already mapped in %s", domain, serverConfigPath))
10831105
}
10841106

1107+
if isTopLevelSiteDomain(domain) {
1108+
wwwDomain := "www." + domain
1109+
if hasDomainMapLine(cfg, wwwDomain) {
1110+
return apperr.New(apperr.CodeValidation, fmt.Sprintf("domain %s requires alias %s but alias is already mapped in %s", domain, wwwDomain, serverConfigPath))
1111+
}
1112+
}
1113+
10851114
return nil
10861115
}
10871116

@@ -1096,17 +1125,18 @@ func (s SiteService) registerDomainInServerConfig(domain, siteRoot, vhostConfigP
10961125
return apperr.New(apperr.CodeValidation, fmt.Sprintf("domain %s already exists in %s", domain, serverConfigPath))
10971126
}
10981127

1128+
mapDomains := mappedListenerDomains(domain)
10991129
updated := strings.TrimRight(cfg, "\n") + "\n\n" + buildVHostDefinition(domain, siteRoot, vhostConfigPath) + "\n"
11001130
lines := strings.Split(updated, "\n")
11011131
httpListenerName := chooseExistingListenerName(lines, []string{"HTTP", "Default"}, "HTTP")
11021132
httpsListenerName := chooseExistingListenerName(lines, []string{"HTTPS", "SSL"}, "HTTPS")
11031133

1104-
updated, mappedHTTP := ensureDomainMappedInNamedListener(updated, httpListenerName, domain)
1105-
updated, mappedHTTPS := ensureDomainMappedInNamedListener(updated, httpsListenerName, domain)
1134+
updated, mappedHTTP := ensureDomainMappedInNamedListener(updated, httpListenerName, domain, mapDomains)
1135+
updated, mappedHTTPS := ensureDomainMappedInNamedListener(updated, httpsListenerName, domain, mapDomains)
11061136

11071137
mappedFallback := false
11081138
if !mappedHTTP && !mappedHTTPS {
1109-
updated, mappedFallback, err = ensureDomainMappedInFirstListener(updated, domain)
1139+
updated, mappedFallback, err = ensureDomainMappedInFirstListener(updated, domain, mapDomains)
11101140
if err != nil {
11111141
return err
11121142
}
@@ -1118,13 +1148,13 @@ func (s SiteService) registerDomainInServerConfig(domain, siteRoot, vhostConfigP
11181148

11191149
s.console.Bullet("Registered virtual host in " + serverConfigPath)
11201150
if mappedHTTP {
1121-
s.console.Bullet("Mapped domain in HTTP listener (" + httpListenerName + "): " + domain)
1151+
s.console.Bullet("Mapped domain in HTTP listener (" + httpListenerName + "): " + strings.Join(mapDomains, ", "))
11221152
}
11231153
if mappedHTTPS {
1124-
s.console.Bullet("Mapped domain in HTTPS listener (" + httpsListenerName + "): " + domain)
1154+
s.console.Bullet("Mapped domain in HTTPS listener (" + httpsListenerName + "): " + strings.Join(mapDomains, ", "))
11251155
}
11261156
if mappedFallback {
1127-
s.console.Bullet("Mapped domain in first listener: " + domain)
1157+
s.console.Bullet("Mapped domain in first listener: " + strings.Join(mapDomains, ", "))
11281158
}
11291159
return nil
11301160
}
@@ -1140,7 +1170,11 @@ func (s SiteService) removeDomainFromServerConfig(domain, serverConfigPath strin
11401170
if err != nil {
11411171
return err
11421172
}
1143-
updated, removedMaps := removeDomainMappings(updated, domain)
1173+
removeDomains := []string{domain}
1174+
if isTopLevelSiteDomain(domain) {
1175+
removeDomains = append(removeDomains, "www."+domain)
1176+
}
1177+
updated, removedMaps := removeDomainMappings(updated, removeDomains...)
11441178
if !removedVHost && !removedMaps {
11451179
s.console.Warn("No matching virtualhost/map entries found for " + domain)
11461180
return nil
@@ -1154,40 +1188,44 @@ func (s SiteService) removeDomainFromServerConfig(domain, serverConfigPath strin
11541188
return nil
11551189
}
11561190

1157-
func ensureDomainMappedInNamedListener(cfg, listenerName, domain string) (string, bool) {
1191+
func ensureDomainMappedInNamedListener(cfg, listenerName, domain string, mapDomains []string) (string, bool) {
11581192
lines := strings.Split(cfg, "\n")
11591193
start, end := findListenerBlockByName(lines, listenerName)
11601194
if start < 0 || end < 0 {
11611195
return cfg, false
11621196
}
11631197

11641198
for i := start; i <= end; i++ {
1165-
if mapLineContainsDomain(strings.TrimSpace(lines[i]), domain) {
1166-
return cfg, false
1199+
for _, mapDomain := range mapDomains {
1200+
if mapLineContainsDomain(strings.TrimSpace(lines[i]), mapDomain) {
1201+
return cfg, false
1202+
}
11671203
}
11681204
}
11691205

11701206
indent := detectMapIndent(lines[start : end+1])
1171-
mapLine := fmt.Sprintf("%smap %s %s", indent, domain, domain)
1207+
mapLine := fmt.Sprintf("%smap %s %s", indent, domain, strings.Join(mapDomains, ", "))
11721208
lines = append(lines[:end], append([]string{mapLine}, lines[end:]...)...)
11731209
return strings.Join(lines, "\n"), true
11741210
}
11751211

1176-
func ensureDomainMappedInFirstListener(cfg, domain string) (string, bool, error) {
1212+
func ensureDomainMappedInFirstListener(cfg, domain string, mapDomains []string) (string, bool, error) {
11771213
lines := strings.Split(cfg, "\n")
11781214
start, end := findFirstListenerBlock(lines)
11791215
if start < 0 || end < 0 {
11801216
return "", false, apperr.New(apperr.CodeConfig, "no listener block found in OpenLiteSpeed server config; cannot auto-map domain")
11811217
}
11821218

11831219
for i := start; i <= end; i++ {
1184-
if mapLineContainsDomain(strings.TrimSpace(lines[i]), domain) {
1185-
return cfg, false, nil
1220+
for _, mapDomain := range mapDomains {
1221+
if mapLineContainsDomain(strings.TrimSpace(lines[i]), mapDomain) {
1222+
return cfg, false, nil
1223+
}
11861224
}
11871225
}
11881226

11891227
indent := detectMapIndent(lines[start : end+1])
1190-
mapLine := fmt.Sprintf("%smap %s %s", indent, domain, domain)
1228+
mapLine := fmt.Sprintf("%smap %s %s", indent, domain, strings.Join(mapDomains, ", "))
11911229
lines = append(lines[:end], append([]string{mapLine}, lines[end:]...)...)
11921230
return strings.Join(lines, "\n"), true, nil
11931231
}
@@ -1300,7 +1338,19 @@ func findVirtualHostBlockByName(lines []string, domain string) (int, int) {
13001338
return -1, -1
13011339
}
13021340

1303-
func removeDomainMappings(cfg, domain string) (string, bool) {
1341+
func removeDomainMappings(cfg string, domains ...string) (string, bool) {
1342+
removeSet := map[string]struct{}{}
1343+
for _, domain := range domains {
1344+
normalized := strings.TrimSpace(strings.ToLower(domain))
1345+
if normalized == "" {
1346+
continue
1347+
}
1348+
removeSet[normalized] = struct{}{}
1349+
}
1350+
if len(removeSet) == 0 {
1351+
return cfg, false
1352+
}
1353+
13041354
lines := strings.Split(cfg, "\n")
13051355
changed := false
13061356
for i, line := range lines {
@@ -1321,7 +1371,7 @@ func removeDomainMappings(cfg, domain string) (string, bool) {
13211371
if host == "" {
13221372
continue
13231373
}
1324-
if strings.EqualFold(host, domain) {
1374+
if _, ok := removeSet[strings.ToLower(host)]; ok {
13251375
removed = true
13261376
continue
13271377
}
@@ -1340,7 +1390,7 @@ func removeDomainMappings(cfg, domain string) (string, bool) {
13401390
if idx := strings.Index(line, "map"); idx > 0 {
13411391
indent = line[:idx]
13421392
}
1343-
lines[i] = fmt.Sprintf("%smap %s %s", indent, mapName, strings.Join(keptHosts, ","))
1393+
lines[i] = fmt.Sprintf("%smap %s %s", indent, mapName, strings.Join(keptHosts, ", "))
13441394
}
13451395
if !changed {
13461396
return cfg, false
@@ -2097,23 +2147,30 @@ func (s SiteService) ensureCertbotAvailable(ctx context.Context, info platform.I
20972147
return nil
20982148
}
20992149

2100-
func (s SiteService) issueLetsEncryptCertificate(ctx context.Context, info platform.Info, domain, webRoot string) (string, string, error) {
2150+
func (s SiteService) issueLetsEncryptCertificate(ctx context.Context, info platform.Info, webRoot string, domains ...string) (string, string, error) {
21012151
if err := s.ensureCertbotAvailable(ctx, info); err != nil {
21022152
return "", "", err
21032153
}
21042154

2105-
res, err := s.runner.Run(
2106-
ctx,
2107-
"certbot",
2155+
certDomains := uniqueNormalizedDomains(domains)
2156+
if len(certDomains) == 0 {
2157+
return "", "", apperr.New(apperr.CodeValidation, "at least one domain is required for Let's Encrypt issuance")
2158+
}
2159+
2160+
certbotArgs := []string{
21082161
"certonly",
21092162
"--non-interactive",
21102163
"--agree-tos",
21112164
"--register-unsafely-without-email",
21122165
"--keep-until-expiring",
21132166
"--webroot",
21142167
"-w", webRoot,
2115-
"-d", domain,
2116-
)
2168+
}
2169+
for _, domain := range certDomains {
2170+
certbotArgs = append(certbotArgs, "-d", domain)
2171+
}
2172+
2173+
res, err := s.runner.Run(ctx, "certbot", certbotArgs...)
21172174
if err != nil {
21182175
detail := strings.TrimSpace(strings.Join([]string{res.Stdout, res.Stderr}, "\n"))
21192176
if detail != "" {
@@ -2122,7 +2179,8 @@ func (s SiteService) issueLetsEncryptCertificate(ctx context.Context, info platf
21222179
return "", "", apperr.Wrap(apperr.CodeCommand, "certbot certificate issuance failed", err)
21232180
}
21242181

2125-
certFile, keyFile := letsEncryptCertPaths(domain)
2182+
primaryDomain := certDomains[0]
2183+
certFile, keyFile := letsEncryptCertPaths(primaryDomain)
21262184
if !fileExists(certFile) || !fileExists(keyFile) {
21272185
return "", "", apperr.New(
21282186
apperr.CodeCommand,
@@ -2643,6 +2701,59 @@ func wordPressBaseURL(domain string, secure bool) string {
26432701
return scheme + "://" + strings.TrimSpace(strings.ToLower(domain))
26442702
}
26452703

2704+
func isTopLevelSiteDomain(domain string) bool {
2705+
d := strings.TrimSpace(strings.ToLower(domain))
2706+
if d == "" {
2707+
return false
2708+
}
2709+
labels := strings.Split(d, ".")
2710+
if len(labels) < 2 {
2711+
return false
2712+
}
2713+
if len(labels) == 2 {
2714+
return true
2715+
}
2716+
if len(labels) == 3 {
2717+
sld := labels[len(labels)-2]
2718+
tld := labels[len(labels)-1]
2719+
if len(tld) == 2 {
2720+
switch sld {
2721+
case "ac", "co", "com", "edu", "gov", "mil", "net", "org":
2722+
return true
2723+
}
2724+
}
2725+
}
2726+
return false
2727+
}
2728+
2729+
func mappedListenerDomains(domain string) []string {
2730+
d := strings.TrimSpace(strings.ToLower(domain))
2731+
if d == "" {
2732+
return nil
2733+
}
2734+
if isTopLevelSiteDomain(d) {
2735+
return []string{"www." + d, d}
2736+
}
2737+
return []string{d}
2738+
}
2739+
2740+
func uniqueNormalizedDomains(domains []string) []string {
2741+
out := make([]string, 0, len(domains))
2742+
seen := map[string]struct{}{}
2743+
for _, domain := range domains {
2744+
normalized := strings.TrimSpace(strings.ToLower(domain))
2745+
if normalized == "" {
2746+
continue
2747+
}
2748+
if _, ok := seen[normalized]; ok {
2749+
continue
2750+
}
2751+
seen[normalized] = struct{}{}
2752+
out = append(out, normalized)
2753+
}
2754+
return out
2755+
}
2756+
26462757
func (s SiteService) createWordPressDatabase(ctx context.Context, dbName, dbUser, dbPassword string) error {
26472758
sql := fmt.Sprintf(
26482759
"CREATE DATABASE IF NOT EXISTS `%s`; CREATE USER IF NOT EXISTS '%s'@'localhost' IDENTIFIED BY '%s'; GRANT ALL PRIVILEGES ON `%s`.* TO '%s'@'localhost'; FLUSH PRIVILEGES;",

0 commit comments

Comments
 (0)