Files
havline-native/app-src/internal/nginx/generate.go
T

284 lines
9.0 KiB
Go
Raw Normal View History

package nginx
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/nginxtmpl"
"github.com/havline/havline/internal/proxy"
)
func Generate(cfg config.Config, rules []proxy.Rule, certs []CertSource, opts GenerateOptions) (string, error) {
var b strings.Builder
b.WriteString(`worker_processes auto;
error_log ` + absNginxPath(filepath.Join(cfg.LogsDir(), "error.log")) + ` warn;
pid ` + absNginxPath(cfg.NginxPIDFile) + `;
events {
worker_connections 1024;
}
http {
include ` + absNginxPath(cfg.NginxMimeTypes) + `;
default_type application/octet-stream;
client_body_temp_path ` + absNginxPath(filepath.Join(cfg.NginxDir(), "temp", "client_body_temp")) + `;
proxy_temp_path ` + absNginxPath(filepath.Join(cfg.NginxDir(), "temp", "proxy_temp")) + `;
fastcgi_temp_path ` + absNginxPath(filepath.Join(cfg.NginxDir(), "temp", "fastcgi_temp")) + `;
uwsgi_temp_path ` + absNginxPath(filepath.Join(cfg.NginxDir(), "temp", "uwsgi_temp")) + `;
scgi_temp_path ` + absNginxPath(filepath.Join(cfg.NginxDir(), "temp", "scgi_temp")) + `;
log_format havline_access '$time_iso8601 $host $server_port $request_method $request_uri $status $request_time $remote_addr $upstream_addr $request_length $bytes_sent';
access_log ` + absNginxPath(filepath.Join(cfg.LogsDir(), "access.log")) + ` havline_access;
sendfile on;
keepalive_timeout 65;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
limit_req_status 429;
limit_conn_status 503;
`)
writeRealIPDirectives(&b, opts.TrustedProxy)
writeGlobalAllowList(&b, opts.GlobalIPWhitelist)
writeGlobalDenyList(&b, opts.GlobalIPBlacklist)
writeLimitZones(&b, rules)
writeChinaGeoBlocks(&b, cfg, opts, rules)
writeChinaOnlyBypassGeo(&b, rules, opts)
writeGlobalCustomInclude(&b, cfg, opts)
for _, rule := range rules {
if !rule.Enabled {
continue
}
if rule.NginxMode == "custom" {
if err := writeCustomRuleBlocks(&b, cfg, rule.ID, opts); err != nil {
return "", err
}
continue
}
if err := writeRuleBlocks(&b, cfg, rule, certs, opts); err != nil {
return "", err
}
}
b.WriteString("}\n")
content := b.String()
if err := assertValidSSLBlocks(content); err != nil {
return "", err
}
return content, nil
}
func GenerateRuleBlocks(cfg config.Config, rule proxy.Rule, certs []CertSource, opts GenerateOptions) (string, error) {
var b strings.Builder
writeRulePreambleComments(&b, rule)
if err := writeRuleBlocks(&b, cfg, rule, certs, opts); err != nil {
return "", err
}
content := b.String()
if err := assertValidSSLBlocks(content); err != nil {
return "", err
}
return content, nil
}
func writeGlobalCustomInclude(b *strings.Builder, cfg config.Config, opts GenerateOptions) {
if strings.TrimSpace(opts.GlobalCustomOverride) != "" {
writeIndentedHTTPSnippet(b, opts.GlobalCustomOverride)
return
}
if globalCustomShouldInclude(cfg, opts) {
path := globalCustomPath(cfg, opts)
b.WriteString(fmt.Sprintf(" include %s;\n\n", absNginxPath(path)))
return
}
writeIndentedHTTPSnippet(b, DefaultGlobalHTTPSnippet())
}
func writeIndentedHTTPSnippet(b *strings.Builder, content string) {
for _, line := range strings.Split(strings.TrimRight(content, "\n"), "\n") {
b.WriteString(" " + line + "\n")
}
b.WriteString("\n")
}
func writeCustomRuleBlocks(b *strings.Builder, cfg config.Config, ruleID int64, opts GenerateOptions) error {
content := strings.TrimSpace(ruleCustomContent(cfg, ruleID, opts))
if content == "" {
return fmt.Errorf("规则 %d 的自定义 Nginx 配置为空", ruleID)
}
if !strings.HasSuffix(content, "\n") {
content += "\n"
}
b.WriteString(content)
return nil
}
func writeRuleBlocks(b *strings.Builder, cfg config.Config, rule proxy.Rule, certs []CertSource, opts GenerateOptions) error {
for _, group := range rule.PortGroups() {
if len(group.Hostnames) == 0 {
continue
}
serverNames := strings.Join(group.Hostnames, " ")
cert := findCertificateForHosts(cfg.CertsDir(), group.Hostnames, certs)
useHTTPS := rule.HTTPSEnabled && certUsable(cert)
wroteBlock := false
if useHTTPS {
if !appendSSLServerBlock(b, cfg, group.Port, serverNames, cert, rule, opts) {
return fmt.Errorf("HTTPS 证书文件不可用,请重新导入证书或关闭 HTTPS")
}
wroteBlock = true
}
if rule.HTTPRedirect && useHTTPS {
writeServerBlockHeaderComment(b, rule, serverNames, group.Port, "redirect")
b.WriteString("server {\n")
writeListenComments(b, group.Port, false, rule.ListenIPv4, rule.ListenIPv6)
writeListenDirectives(b, group.Port, false, rule.ListenIPv4, rule.ListenIPv6)
b.WriteString(fmt.Sprintf(" server_name %s;\n", serverNames))
writeErrorPages(b, cfg)
writeCommentLine(b, 4, "301 跳转至 HTTPS")
b.WriteString(" return 301 https://$host:$server_port$request_uri;\n")
b.WriteString("}\n")
wroteBlock = true
}
if !wroteBlock {
writeServerBlockHeaderComment(b, rule, serverNames, group.Port, "http")
b.WriteString("server {\n")
writeListenComments(b, group.Port, false, rule.ListenIPv4, rule.ListenIPv6)
writeListenDirectives(b, group.Port, false, rule.ListenIPv4, rule.ListenIPv6)
b.WriteString(fmt.Sprintf(" server_name %s;\n", serverNames))
writeErrorPages(b, cfg)
writeLocationWithSecurity(b, cfg, rule, opts)
b.WriteString("}\n")
}
}
return nil
}
func appendSSLServerBlock(b *strings.Builder, cfg config.Config, port int, serverNames string, cert *certFiles, rule proxy.Rule, opts GenerateOptions) bool {
if !certUsable(cert) {
return false
}
var block strings.Builder
writeServerBlockHeaderComment(&block, rule, serverNames, port, "https")
block.WriteString("server {\n")
writeListenComments(&block, port, true, rule.ListenIPv4, rule.ListenIPv6)
writeListenDirectives(&block, port, true, rule.ListenIPv4, rule.ListenIPv6)
block.WriteString(fmt.Sprintf(" server_name %s;\n\n", serverNames))
writeCommentLine(&block, 4, "SSL 证书")
writeTLSProtocolComment(&block, rule.Security)
block.WriteString(nginxtmpl.SSLDirectives(nginxtmpl.TLSConfig{
CertPath: absNginxPath(cert.CertPath),
KeyPath: absNginxPath(cert.KeyPath),
TLS13Only: rule.Security.TLSMin13Only,
}))
block.WriteString("\n")
writeServerSecurityHeaders(&block, rule.Security, true)
writeErrorPages(&block, cfg)
writeLocationWithSecurity(&block, cfg, rule, opts)
block.WriteString("}\n")
b.WriteString(block.String())
return true
}
func writeLocationWithSecurity(b *strings.Builder, cfg config.Config, rule proxy.Rule, opts GenerateOptions) {
writeLocationHeaderComment(b, rule.Upstream)
b.WriteString(" location / {\n")
writeLocationSecurity(b, cfg, rule, opts)
writeProxyLocationBody(b, rule.Upstream, rule.Security)
b.WriteString(" }\n")
}
func writeProxyLocationBody(b *strings.Builder, upstream string, sec proxy.SecurityConfig) {
sec = sec.Normalize()
writeProxyBodyComments(b, upstream, sec)
hostHeader := "$host"
if sec.ProxyHostUpstream {
hostHeader = "$proxy_host"
}
sslBlock := ""
if strings.HasPrefix(upstream, "https://") {
if sec.ProxySSLVerifyOff {
sslBlock = ` proxy_ssl_verify off;
proxy_ssl_server_name on;
`
} else {
sslBlock = ` proxy_ssl_server_name on;
`
}
}
b.WriteString(fmt.Sprintf(`%s proxy_pass %s;
proxy_http_version 1.1;
proxy_set_header Host %s;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-Proto $scheme;
%s
proxy_buffering off;
proxy_read_timeout 3600s;
`, sslBlock, upstream, hostHeader, nginxtmpl.UpgradeHeaders(" ", "$connection_upgrade")))
}
func writeListenDirectives(b *strings.Builder, port int, ssl bool, ipv4 bool, ipv6 bool) {
sslSuffix := ""
if ssl {
sslSuffix = " ssl"
}
if ipv4 {
b.WriteString(fmt.Sprintf(" listen %d%s;\n", port, sslSuffix))
}
if ipv6 {
b.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, sslSuffix))
}
}
type certFiles struct {
CertPath string
KeyPath string
}
func findCertificate(certsDir, domain string) *certFiles {
parts := strings.Split(domain, ".")
for i := 0; i < len(parts)-1; i++ {
wildcard := "*." + strings.Join(parts[i:], ".")
if c := certAt(certsDir, wildcard); c != nil {
return c
}
}
if c := certAt(certsDir, domain); c != nil {
return c
}
return nil
}
func certAt(certsDir, name string) *certFiles {
safeName := strings.ReplaceAll(name, "*", "wildcard")
dir := filepath.Join(certsDir, safeName)
certPath := filepath.Join(dir, "fullchain.pem")
keyPath := filepath.Join(dir, "privatekey.pem")
if fileExists(certPath) && fileExists(keyPath) {
return &certFiles{CertPath: absNginxPath(certPath), KeyPath: absNginxPath(keyPath)}
}
return nil
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}