feat: rename project from Fonu to Havline
CI / verify (push) Canceled after 0s

This commit is contained in:
2026-10-01 16:54:35 +08:00
parent c377405696
commit 8fa5d922ac
250 changed files with 6724 additions and 5070 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/proxy"
)
func osReadDir(path string) ([]os.DirEntry, error) {
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/proxy"
)
func writeCommentLine(b *strings.Builder, indent int, text string) {
@@ -72,7 +72,7 @@ func writeSecurityHeadersComment(b *strings.Builder) {
}
func writeErrorPagesComment(b *strings.Builder) {
writeCommentLine(b, 4, "Fonu 自定义错误页(403/404/429/5xx)")
writeCommentLine(b, 4, "Havline 自定义错误页(403/404/429/5xx)")
}
func writeLocationHeaderComment(b *strings.Builder, upstream string) {
+5 -5
View File
@@ -8,8 +8,8 @@ import (
"strings"
"time"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/fsutil"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/fsutil"
)
const maxCustomBackups = 10
@@ -220,7 +220,7 @@ client_max_body_size 50m;
}
func GenerateGlobalFramework(cfg config.Config) string {
return fmt.Sprintf(`# 以下部分由 Fonu 自动生成,请勿在此编辑
return fmt.Sprintf(`# 以下部分由 Havline 自动生成,请勿在此编辑
# 主进程
worker_processes auto;
@@ -240,8 +240,8 @@ http {
default_type application/octet-stream;
# 访问日志格式与路径
log_format fonu_access '$time_iso8601 $host $server_port $request_method $request_uri $status $request_time $remote_addr $upstream_addr $request_length $bytes_sent';
access_log %s fonu_access;
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 %s havline_access;
# 传输优化
sendfile on;
+2 -2
View File
@@ -7,8 +7,8 @@ import (
"testing"
"time"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateRuleBlocks(t *testing.T) {
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"strconv"
"strings"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/proxy"
)
// ParseServerEndpoints 从 Nginx 配置文本里抽取各 server 块的 (server_name, listen 端口) 组合,
+1 -1
View File
@@ -47,7 +47,7 @@ func TestParseServerEndpointsDefaultsToPort80(t *testing.T) {
func TestParseServerEndpointsIgnoresUnparseable(t *testing.T) {
content := `include /etc/nginx/snippets/extra.conf;
server {
listen unix:/run/fonu.sock;
listen unix:/run/havline.sock;
server_name $host;
}
`
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"github.com/fonu/fonu/internal/config"
"github.com/havline/havline/internal/config"
)
func EnsureErrorPages(cfg config.Config) error {
@@ -112,7 +112,7 @@ func copyFileIfChanged(src, dst string) error {
func errorPageHTML(code int, title, message, sprite, bgPos string) string {
var imgBlock string
if sprite == "429" {
imgBlock = `<img class="illus" src="/fonu-errors/429.png" alt="">`
imgBlock = `<img class="illus" src="/havline-errors/429.png" alt="">`
} else {
imgBlock = fmt.Sprintf(`<div class="sprite" role="img" aria-label="%d"></div>`, code)
}
@@ -126,7 +126,7 @@ func errorPageHTML(code int, title, message, sprite, bgPos string) string {
*{box-sizing:border-box;margin:0;padding:0}
body{min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f1419;color:#e2e8f0;padding:24px}
.card{text-align:center;max-width:420px;width:100%%}
.sprite{width:280px;height:186px;margin:0 auto 24px;background:url(/fonu-errors/error.png) no-repeat;background-size:200%% 200%%;background-position:%s}
.sprite{width:280px;height:186px;margin:0 auto 24px;background:url(/havline-errors/error.png) no-repeat;background-size:200%% 200%%;background-position:%s}
.illus{width:280px;height:auto;margin:0 auto 24px;display:block}
.code{font-size:14px;font-weight:600;color:#22c55e;letter-spacing:.08em;margin-bottom:8px}
h1{font-size:22px;font-weight:600;margin-bottom:8px}
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/havline/havline/internal/config"
)
func TestEnsureErrorPagesWritesAssets(t *testing.T) {
@@ -35,8 +35,8 @@ func TestEnsureErrorPagesWritesAssets(t *testing.T) {
}
body := string(html)
for _, want := range []string{
"/fonu-errors/error.png",
"background:url(/fonu-errors/error.png)",
"/havline-errors/error.png",
"background:url(/havline-errors/error.png)",
} {
if !strings.Contains(body, want) {
t.Fatalf("403.html missing %q:\n%s", want, body)
+5 -5
View File
@@ -6,9 +6,9 @@ import (
"path/filepath"
"strings"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/nginxtmpl"
"github.com/fonu/fonu/internal/proxy"
"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) {
@@ -26,8 +26,8 @@ http {
include ` + absNginxPath(cfg.NginxMimeTypes) + `;
default_type application/octet-stream;
log_format fonu_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")) + ` fonu_access;
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;
+4 -4
View File
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestChinaOnlyBypassGeoWithoutUserWhitelist(t *testing.T) {
@@ -44,10 +44,10 @@ func TestChinaOnlyBypassGeoWithoutUserWhitelist(t *testing.T) {
t.Fatalf("generate: %v", err)
}
for _, want := range []string{
"geo $fonu_client_ip $fonu_rule_3_china_bypass",
"geo $havline_client_ip $havline_rule_3_china_bypass",
"192.168.0.0/16 1;",
"10.0.0.0/8 1;",
"if ($fonu_rule_3_china_bypass = 1)",
"if ($havline_rule_3_china_bypass = 1)",
} {
if !strings.Contains(content, want) {
t.Fatalf("missing %q in:\n%s", want, content)
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateChinaCIDRUsesFinalPathWhenOverrideCleared(t *testing.T) {
+4 -4
View File
@@ -4,15 +4,15 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateRuleBlocksIncludesComments(t *testing.T) {
cfg := config.Config{DataDir: t.TempDir()}
rule := proxy.Rule{
ID: 3,
Name: "fonu",
Name: "havline",
Upstream: "http://127.0.0.1:8080",
ListenPort: 8011,
ListenIPv4: true,
@@ -32,7 +32,7 @@ func TestGenerateRuleBlocksIncludesComments(t *testing.T) {
t.Fatal(err)
}
for _, want := range []string{
"# 规则:fonu",
"# 规则:havline",
"# 上游:http://127.0.0.1:8080",
"# 限流 zone 在 http 块中自动生成",
"# IP 黑名单",
@@ -4,8 +4,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateHTTPSWithoutCertUsesHTTPOnly(t *testing.T) {
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateHTTPSWithCertIncludesCertificateDirectives(t *testing.T) {
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateMultiHostCustomPort(t *testing.T) {
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateHTTPSBlockBeforeHTTPRedirect(t *testing.T) {
+14 -14
View File
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateSecurityDirectives(t *testing.T) {
@@ -64,25 +64,25 @@ func TestGenerateSecurityDirectives(t *testing.T) {
"allow 10.0.0.0/8;",
"allow 203.0.113.10;",
"deny 8.8.8.8;",
"limit_req_zone $binary_remote_addr zone=fonu_rule_7_req",
"limit_conn_zone $binary_remote_addr zone=fonu_rule_7_conn",
"limit_req_zone $binary_remote_addr zone=havline_rule_7_req",
"limit_conn_zone $binary_remote_addr zone=havline_rule_7_conn",
"deny 1.2.3.4;",
"geo $fonu_client_ip $fonu_is_private",
"geo $fonu_client_ip $fonu_rule_7_china_bypass",
"geo $havline_client_ip $havline_is_private",
"geo $havline_client_ip $havline_rule_7_china_bypass",
"192.168.0.0/16 1;",
"192.168.1.0/24 1;",
"203.0.113.10 1;",
"set $fonu_china_block 0;",
"if ($fonu_is_private = 1)",
"if ($fonu_rule_7_china_bypass = 1)",
"set $havline_china_block 0;",
"if ($havline_is_private = 1)",
"if ($havline_rule_7_china_bypass = 1)",
"allow 192.168.1.0/24;",
"deny all;",
"auth_basic",
"limit_req zone=fonu_rule_7_req burst=20",
"limit_conn fonu_rule_7_conn 20",
"error_page 403 /fonu-errors/403.html",
"location = /fonu-errors/error.png",
"location = /fonu-errors/429.png",
"limit_req zone=havline_rule_7_req burst=20",
"limit_conn havline_rule_7_conn 20",
"error_page 403 /havline-errors/403.html",
"location = /havline-errors/error.png",
"location = /havline-errors/429.png",
"X-Forwarded-Host $host",
"X-Forwarded-Port $server_port",
"X-Real-Proto $scheme",
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"path/filepath"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateMixedRulesWithoutCerts(t *testing.T) {
+2 -2
View File
@@ -4,8 +4,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestGenerateIncludesWebSocketAndProxy(t *testing.T) {
+2 -2
View File
@@ -4,14 +4,14 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/havline/havline/internal/config"
)
func TestGenerateGlobalFrameworkIncludesComments(t *testing.T) {
cfg := config.Config{DataDir: t.TempDir()}
out := GenerateGlobalFramework(cfg)
for _, want := range []string{
"# 以下部分由 Fonu 自动生成",
"# 以下部分由 Havline 自动生成",
"# 主进程",
"# MIME 类型",
"# 访问日志格式与路径",
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"path/filepath"
"strings"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
// nginxHtpasswdHash normalizes bcrypt hashes for nginx/apr auth_basic.
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestSyncHtpasswdFilesWritesBasicAuth(t *testing.T) {
+3 -3
View File
@@ -12,9 +12,9 @@ import (
"syscall"
"time"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/fsutil"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/fsutil"
"github.com/havline/havline/internal/proxy"
)
const (
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"path/filepath"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestApplyWithNginxIfAvailable(t *testing.T) {
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"path/filepath"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/proxy"
)
func TestApplyWithoutNginxBinary(t *testing.T) {
@@ -20,7 +20,7 @@ func TestApplyWithoutNginxBinary(t *testing.T) {
cfg := config.Config{
DataDir: dir,
NginxBin: "fonu-missing-nginx-binary",
NginxBin: "havline-missing-nginx-binary",
NginxPIDFile: filepath.Join(dir, "nginx", "nginx.pid"),
NginxMimeTypes: mimePath,
}
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"path/filepath"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/havline/havline/internal/config"
)
func TestReadPIDFile(t *testing.T) {
+2 -2
View File
@@ -80,7 +80,7 @@ func writeRealIPDirectives(b *strings.Builder, cfg TrustedProxyConfig) {
writeRealIPComment(b, cfg)
cidrs := cfg.effectiveCIDRs()
if len(cidrs) == 0 {
b.WriteString(" map $remote_addr $fonu_client_ip {\n")
b.WriteString(" map $remote_addr $havline_client_ip {\n")
b.WriteString(" default $remote_addr;\n")
b.WriteString(" }\n\n")
return
@@ -93,7 +93,7 @@ func writeRealIPDirectives(b *strings.Builder, cfg TrustedProxyConfig) {
if cfg.Recursive || header == "X-Forwarded-For" {
b.WriteString(" real_ip_recursive on;\n")
}
b.WriteString("\n map $remote_addr $fonu_client_ip {\n")
b.WriteString("\n map $remote_addr $havline_client_ip {\n")
b.WriteString(" default $remote_addr;\n")
b.WriteString(" }\n\n")
}
+26 -26
View File
@@ -5,9 +5,9 @@ import (
"os"
"strings"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/nginxtmpl"
"github.com/fonu/fonu/internal/proxy"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/nginxtmpl"
"github.com/havline/havline/internal/proxy"
)
type GenerateOptions struct {
@@ -53,11 +53,11 @@ func writeLimitZones(b *strings.Builder, rules []proxy.Rule) {
writeLimitZoneComments(b, rule)
}
if rule.Security.RateLimitEnabled() {
zone := fmt.Sprintf("fonu_rule_%d_req", rule.ID)
zone := fmt.Sprintf("havline_rule_%d_req", rule.ID)
writeIndentedZone(b, nginxtmpl.RateLimitZone(zone, rule.Security.RateLimit.Rate))
}
if rule.Security.ConnLimitEnabled() {
writeIndentedZone(b, nginxtmpl.ConnLimitZone(fmt.Sprintf("fonu_rule_%d_conn", rule.ID)))
writeIndentedZone(b, nginxtmpl.ConnLimitZone(fmt.Sprintf("havline_rule_%d_conn", rule.ID)))
}
}
if hasLimitZones(rules) {
@@ -82,7 +82,7 @@ func hasLimitZones(rules []proxy.Rule) bool {
}
func writePrivateIPGeo(b *strings.Builder) {
b.WriteString(` geo $fonu_client_ip $fonu_is_private {
b.WriteString(` geo $havline_client_ip $havline_is_private {
default 0;
10.0.0.0/8 1;
127.0.0.0/8 1;
@@ -115,7 +115,7 @@ func writeChinaGeoBlocks(b *strings.Builder, cfg config.Config, opts GenerateOpt
if opts.ChinaCIDRPathOverride != "" {
cidrPath = absNginxPath(opts.ChinaCIDRPathOverride)
}
b.WriteString(fmt.Sprintf(` geo $fonu_client_ip $fonu_is_china {
b.WriteString(fmt.Sprintf(` geo $havline_client_ip $havline_is_china {
default 0;
include %s;
}
@@ -134,7 +134,7 @@ func writeChinaOnlyBypassGeo(b *strings.Builder, rules []proxy.Rule, opts Genera
sec := rule.Security.Normalize()
cidrs := chinaBypassCIDRs(sec, opts.GlobalIPWhitelist)
writeChinaBypassComment(b, rule)
b.WriteString(fmt.Sprintf(" geo $fonu_client_ip $fonu_rule_%d_china_bypass {\n", rule.ID))
b.WriteString(fmt.Sprintf(" geo $havline_client_ip $havline_rule_%d_china_bypass {\n", rule.ID))
b.WriteString(" default 0;\n")
for _, cidr := range cidrs {
b.WriteString(fmt.Sprintf(" %s 1;\n", cidr))
@@ -149,25 +149,25 @@ func chinaOnlyCheckDirectives(rule proxy.Rule, opts GenerateOptions) string {
return ""
}
if !opts.ChinaCIDRAvailable {
return ` if ($fonu_is_private = 0) {
return ` if ($havline_is_private = 0) {
return 403;
}
`
}
var b strings.Builder
b.WriteString(` set $fonu_china_block 0;
if ($fonu_is_china = 0) {
set $fonu_china_block 1;
b.WriteString(` set $havline_china_block 0;
if ($havline_is_china = 0) {
set $havline_china_block 1;
}
if ($fonu_is_private = 1) {
set $fonu_china_block 0;
if ($havline_is_private = 1) {
set $havline_china_block 0;
}
`)
b.WriteString(fmt.Sprintf(` if ($fonu_rule_%d_china_bypass = 1) {
set $fonu_china_block 0;
b.WriteString(fmt.Sprintf(` if ($havline_rule_%d_china_bypass = 1) {
set $havline_china_block 0;
}
`, rule.ID))
b.WriteString(` if ($fonu_china_block = 1) {
b.WriteString(` if ($havline_china_block = 1) {
return 403;
}
`)
@@ -188,20 +188,20 @@ func writeErrorPages(b *strings.Builder, cfg config.Config) {
writeErrorPagesComment(b)
// HTML is served via internal error_page redirect; images are fetched by the browser
// as separate requests and must not use the internal-only location.
b.WriteString(` error_page 403 /fonu-errors/403.html;
error_page 404 /fonu-errors/404.html;
error_page 429 /fonu-errors/429.html;
error_page 500 /fonu-errors/500.html;
error_page 502 503 /fonu-errors/503.html;
b.WriteString(` error_page 403 /havline-errors/403.html;
error_page 404 /havline-errors/404.html;
error_page 429 /havline-errors/429.html;
error_page 500 /havline-errors/500.html;
error_page 502 503 /havline-errors/503.html;
location = /fonu-errors/error.png {
location = /havline-errors/error.png {
alias ` + errorsDir + `/error.png;
}
location = /fonu-errors/429.png {
location = /havline-errors/429.png {
alias ` + errorsDir + `/429.png;
}
location ^~ /fonu-errors/ {
location ^~ /havline-errors/ {
internal;
alias ` + errorsDir + `/;
}
@@ -222,7 +222,7 @@ func writeLocationSecurity(b *strings.Builder, cfg config.Config, rule proxy.Rul
}
b.WriteString(nginxtmpl.AccessDirectives(acl, " "))
limits := nginxtmpl.LimitConfig{Zone: fmt.Sprintf("fonu_rule_%d", rule.ID)}
limits := nginxtmpl.LimitConfig{Zone: fmt.Sprintf("havline_rule_%d", rule.ID)}
if sec.ConnLimitEnabled() {
limits.Conn = sec.ConnLimit.Max
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"os"
"time"
"github.com/fonu/fonu/internal/fsutil"
"github.com/havline/havline/internal/fsutil"
)
// maxConfigVersions 主配置保留的版本份数。Apply 每次保存都会留一份,fsutil 会跳过内容相同的版本,
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
"github.com/fonu/fonu/internal/config"
"github.com/fonu/fonu/internal/fsutil"
"github.com/havline/havline/internal/config"
"github.com/havline/havline/internal/fsutil"
)
func newVersionTestManager(t *testing.T) (*Manager, config.Config) {
@@ -18,7 +18,7 @@ func newVersionTestManager(t *testing.T) (*Manager, config.Config) {
dir := t.TempDir()
cfg := config.Config{
DataDir: dir,
NginxBin: "fonu-nginx-not-installed",
NginxBin: "havline-nginx-not-installed",
NginxPIDFile: filepath.Join(dir, "nginx", "nginx.pid"),
NginxMimeTypes: filepath.Join(dir, "mime.types"),
}