feat: create standalone native fnOS repository
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
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;
|
||||
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user