diff --git a/openflare_agent/internal/nginx/manager.go b/openflare_agent/internal/nginx/manager.go index 0ba39039..445010a2 100644 --- a/openflare_agent/internal/nginx/manager.go +++ b/openflare_agent/internal/nginx/manager.go @@ -963,7 +963,7 @@ func certFileMode(relativePath string) fs.FileMode { switch strings.ToLower(filepath.Ext(strings.TrimSpace(relativePath))) { case ".crt", ".pem": return 0o644 - case ".key", ".htpasswd": + case ".key": return 0o600 default: return 0o644 diff --git a/openflare_agent/internal/nginx/manager_test.go b/openflare_agent/internal/nginx/manager_test.go index 349c3d1b..2bd460b6 100644 --- a/openflare_agent/internal/nginx/manager_test.go +++ b/openflare_agent/internal/nginx/manager_test.go @@ -722,10 +722,9 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { Executor: &fakeExecutor{}, } - outcome := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\n__OPENFLARE_RESOLVER_DIRECTIVE__server { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;\nauth_basic_user_file __OPENFLARE_CERT_DIR__/basic_auth/site.htpasswd;", []protocol.SupportFile{ + outcome := manager.Apply(context.Background(), "include __OPENFLARE_ROUTE_CONFIG__;\n__OPENFLARE_RESOLVER_DIRECTIVE__server { listen __OPENFLARE_OBSERVABILITY_LISTEN__; }", "ssl_certificate __OPENFLARE_CERT_DIR__/1.crt;", []protocol.SupportFile{ {Path: "1.crt", Content: "cert-data"}, {Path: "1.key", Content: "key-data"}, - {Path: "basic_auth/site.htpasswd", Content: "admin:{PLAIN}123\n"}, }) if outcome.Status != ApplyStatusSuccess { t.Fatalf("Apply failed: %#v", outcome) @@ -738,9 +737,6 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if !strings.Contains(string(routeData), "/etc/nginx/openflare-certs/1.crt") { t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData)) } - if !strings.Contains(string(routeData), "/etc/nginx/openflare-certs/basic_auth/site.htpasswd") { - t.Fatalf("expected basic auth file placeholder replacement in route config, got %s", string(routeData)) - } renderedRoute := manager.renderRouteConfig("access_by_lua_file __OPENFLARE_LUA_DIR__/pow/check.lua;\nlocation /.within.website/x/cmd/anubis/static/ { alias __OPENFLARE_POW_STATIC_DIR__/; }\n") if !strings.Contains(renderedRoute, "access_by_lua_file /etc/nginx/openflare-lua/pow/check.lua;") { t.Fatalf("expected lua dir placeholder replacement in route config, got %s", renderedRoute) @@ -765,13 +761,6 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if string(certData) != "cert-data" { t.Fatalf("unexpected cert file content: %s", string(certData)) } - basicAuthData, err := os.ReadFile(filepath.Join(manager.CertDir, "basic_auth", "site.htpasswd")) - if err != nil { - t.Fatalf("failed to read basic auth file: %v", err) - } - if string(basicAuthData) != "admin:{PLAIN}123\n" { - t.Fatalf("unexpected basic auth file content: %s", string(basicAuthData)) - } luaInfo, err := os.Stat(filepath.Join(manager.LuaDir, "log.lua")) if err != nil { t.Fatalf("expected managed lua file to exist, stat err = %v", err) @@ -902,7 +891,6 @@ func TestCertFileMode(t *testing.T) { {path: "1.crt", want: 0o644}, {path: "1.pem", want: 0o644}, {path: "1.key", want: 0o600}, - {path: "basic_auth/site.htpasswd", want: 0o600}, {path: "misc.txt", want: 0o644}, } diff --git a/openflare_server/service/agent.go b/openflare_server/service/agent.go index 0db9c7ad..a942a910 100644 --- a/openflare_server/service/agent.go +++ b/openflare_server/service/agent.go @@ -240,8 +240,6 @@ func filterAgentSupportFiles(files []SupportFile) []SupportFile { switch { case strings.HasSuffix(path, ".crt"), strings.HasSuffix(path, ".key"), strings.HasSuffix(path, ".pem"): filtered = append(filtered, file) - case strings.HasSuffix(path, ".htpasswd"): - filtered = append(filtered, file) case path == "pow_config.json": filtered = append(filtered, file) } diff --git a/openflare_server/service/agent_test.go b/openflare_server/service/agent_test.go index 5de2bbe6..a7126050 100644 --- a/openflare_server/service/agent_test.go +++ b/openflare_server/service/agent_test.go @@ -43,41 +43,6 @@ func TestGetActiveConfigForAgentIncludesPoWConfig(t *testing.T) { } } -func TestGetActiveConfigForAgentIncludesBasicAuthFile(t *testing.T) { - setupServiceTestDB(t) - - _, err := CreateProxyRoute(ProxyRouteInput{ - Domain: "basic-agent.example.com", - OriginURL: "https://origin.internal", - Enabled: true, - BasicAuthEnabled: true, - BasicAuthUsername: "admin", - BasicAuthPassword: "123", - }) - if err != nil { - t.Fatalf("CreateProxyRoute failed: %v", err) - } - - if _, err := PublishConfigVersion("root", false); err != nil { - t.Fatalf("PublishConfigVersion failed: %v", err) - } - - activeConfig, err := GetActiveConfigForAgent() - if err != nil { - t.Fatalf("GetActiveConfigForAgent failed: %v", err) - } - - for _, file := range activeConfig.SupportFiles { - if file.Path == "basic_auth/backend_basic_agent_example_com_1.htpasswd" { - if file.Content != "admin:{PLAIN}123\n" { - t.Fatalf("unexpected basic auth file content: %q", file.Content) - } - return - } - } - t.Fatal("expected agent config to include basic auth htpasswd support file") -} - func TestGetActiveConfigForAgentUsesTenMinutePoWSessionDefault(t *testing.T) { setupServiceTestDB(t) diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index 70c89c02..35b3c45e 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -2,6 +2,7 @@ package service import ( "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -174,8 +175,6 @@ const ( nginxObservabilityPortPlaceholder = "__OPENFLARE_OBSERVABILITY_PORT__" ) -const basicAuthSupportDir = "basic_auth" - var requiredMainConfigTemplatePlaceholders = []string{ "{{OpenRestyWorkerProcesses}}", "{{OpenRestyWorkerConnections}}", @@ -937,19 +936,11 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) LimitRate: route.LimitRate, } upstreamConfig := buildRouteUpstreamConfig(route, upstreams) - basicAuthFilePath := "" - if route.BasicAuthEnabled { - basicAuthFilePath = buildBasicAuthSupportFilePath(route) - supportFiles = append(supportFiles, SupportFile{ - Path: basicAuthFilePath, - Content: renderBasicAuthSupportFile(route.BasicAuthUsername, route.BasicAuthPassword), - }) - } if upstreamConfig.UsesNamedUpstream { builder.WriteString(renderNamedUpstreamBlock(upstreamConfig)) } if !route.EnableHTTPS { - builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, basicAuthFilePath, cfg)) + builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) continue } certIDs, err := decodeStoredCertIDs(route.CertIDs, route.CertID) @@ -1010,7 +1001,7 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) if route.RedirectHTTP { if len(httpOnlyDomains) > 0 { - builder.WriteString(renderHTTPProxyServer(renderServerNames(httpOnlyDomains), route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, basicAuthFilePath, cfg)) + builder.WriteString(renderHTTPProxyServer(renderServerNames(httpOnlyDomains), route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } for _, certID := range certIDs { assignedDomains := domainsByCertID[certID] @@ -1020,14 +1011,14 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) builder.WriteString(renderHTTPRedirectServer(renderServerNames(assignedDomains))) } } else { - builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, basicAuthFilePath, cfg)) + builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } for _, certID := range certIDs { assignedDomains := domainsByCertID[certID] if len(assignedDomains) == 0 { continue } - builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), route.OriginURL, route.OriginHost, certID, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, basicAuthFilePath, cfg)) + builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), route.OriginURL, route.OriginHost, certID, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } } return builder.String(), dedupeSupportFiles(supportFiles), nil @@ -1143,15 +1134,20 @@ func renderPowAccessBlock(powEnabled bool) string { return fmt.Sprintf(" access_by_lua_file %s/pow/check.lua;\n", nginxLuaDirPlaceholder) } -func renderBasicAuthBlock(filePath string) string { - if strings.TrimSpace(filePath) == "" { +func renderBasicAuthBlock(enabled bool, username, password string) string { + if !enabled || username == "" || password == "" { return "" } - return fmt.Sprintf(" auth_basic \"Restricted\";\n auth_basic_user_file %s/%s;\n", nginxCertDirPlaceholder, filePath) -} - -func renderBasicAuthSupportFile(username, password string) string { - return fmt.Sprintf("%s:{PLAIN}%s\n", username, password) + credentials := username + ":" + password + encoded := base64.StdEncoding.EncodeToString([]byte(credentials)) + return fmt.Sprintf(` rewrite_by_lua_block { + local auth = ngx.var.http_authorization + if auth ~= "Basic %s" then + ngx.header["WWW-Authenticate"] = 'Basic realm="Restricted"' + return ngx.exit(401) + end + } +`, encoded) } func renderPowLocationBlocks(powEnabled bool) string { @@ -1281,21 +1277,21 @@ func nextVersionNumber(now time.Time) (string, error) { return fmt.Sprintf("%s-%03d", prefix, count+1), nil } -func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthFilePath string, cfg openRestyConfigSnapshot) string { - return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthFilePath), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) +func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { + return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } func renderHTTPRedirectServer(serverNames string) string { return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", serverNames) } -func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthFilePath string, cfg openRestyConfigSnapshot) string { +func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) - return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthFilePath), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) + return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } -func renderHTTPSServerWithCertificates(serverNames string, originURL string, originHost string, certificateIDs []uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthFilePath string, cfg openRestyConfigSnapshot) string { +func renderHTTPSServerWithCertificates(serverNames string, originURL string, originHost string, certificateIDs []uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { var certificateBlock strings.Builder for _, certificateID := range certificateIDs { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) @@ -1303,7 +1299,7 @@ func renderHTTPSServerWithCertificates(serverNames string, originURL string, ori certificateBlock.WriteString(fmt.Sprintf(" ssl_certificate %s;\n", certPath)) certificateBlock.WriteString(fmt.Sprintf(" ssl_certificate_key %s;\n", keyPath)) } - return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n%s%s%s\n location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certificateBlock.String(), renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthFilePath), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) + return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n%s%s%s\n location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certificateBlock.String(), renderPowAccessBlock(powEnabled), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } func renderServerNames(domains []string) string { @@ -1595,10 +1591,6 @@ func buildRouteUpstreamName(route *model.ProxyRoute) string { return fmt.Sprintf("backend_%s_%d", sanitized, route.ID) } -func buildBasicAuthSupportFilePath(route *model.ProxyRoute) string { - return fmt.Sprintf("%s/%s.htpasswd", basicAuthSupportDir, buildRouteUpstreamName(route)) -} - func renderNamedUpstreamBlock(upstreamConfig routeUpstreamConfig) string { var builder strings.Builder builder.WriteString(fmt.Sprintf("upstream %s {\n", upstreamConfig.Name)) diff --git a/openflare_server/service/https_phase1_test.go b/openflare_server/service/https_phase1_test.go index 1990f0af..7f02c5e2 100644 --- a/openflare_server/service/https_phase1_test.go +++ b/openflare_server/service/https_phase1_test.go @@ -1060,14 +1060,11 @@ func TestPublishConfigVersionRendersBasicAuthWithPoW(t *testing.T) { if err != nil { t.Fatalf("PublishConfigVersion failed: %v", err) } - if strings.Contains(result.Version.RenderedConfig, "rewrite_by_lua_block") { - t.Fatal("expected basic auth to use native nginx auth_basic instead of lua authorization checks") + if !strings.Contains(result.Version.RenderedConfig, `if auth ~= "Basic YWRtaW46MTIz" then`) { + t.Fatal("expected rendered config to include encoded basic auth credentials") } - if !strings.Contains(result.Version.RenderedConfig, `auth_basic "Restricted";`) { - t.Fatal("expected rendered config to enable native basic auth") - } - if !strings.Contains(result.Version.RenderedConfig, "auth_basic_user_file __OPENFLARE_CERT_DIR__/basic_auth/backend_xbot_example_com_1.htpasswd;") { - t.Fatal("expected rendered config to reference managed htpasswd file") + if !strings.Contains(result.Version.RenderedConfig, " return ngx.exit(401)\n end\n }\n") { + t.Fatal("expected rendered basic auth Lua block to close the if statement before the nginx block") } if !strings.Contains(result.Version.RenderedConfig, " access_by_lua_file __OPENFLARE_LUA_DIR__/pow/check.lua;") { t.Fatal("expected PoW access handler to remain at server scope") @@ -1084,23 +1081,6 @@ func TestPublishConfigVersionRendersBasicAuthWithPoW(t *testing.T) { if !strings.Contains(result.Version.SnapshotJSON, `"basic_auth_password":"123"`) { t.Fatal("expected snapshot to include basic auth password") } - var supportFiles []SupportFile - if err := json.Unmarshal([]byte(result.Version.SupportFilesJSON), &supportFiles); err != nil { - t.Fatalf("failed to decode support files: %v", err) - } - foundBasicAuthFile := false - for _, file := range supportFiles { - if file.Path != "basic_auth/backend_xbot_example_com_1.htpasswd" { - continue - } - foundBasicAuthFile = true - if file.Content != "admin:{PLAIN}123\n" { - t.Fatalf("unexpected basic auth support file content: %q", file.Content) - } - } - if !foundBasicAuthFile { - t.Fatal("expected publish to include basic auth htpasswd support file") - } } func TestDiffConfigVersionDetectsBasicAuthChanges(t *testing.T) { diff --git a/openflare_server/service/proxy_route.go b/openflare_server/service/proxy_route.go index e6dd5e57..ee30f2e4 100644 --- a/openflare_server/service/proxy_route.go +++ b/openflare_server/service/proxy_route.go @@ -271,12 +271,6 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro if input.BasicAuthUsername == "" || input.BasicAuthPassword == "" { return nil, errors.New("basic_auth_username and basic_auth_password cannot be empty when basic auth is enabled") } - if strings.ContainsAny(input.BasicAuthUsername, ":\r\n") { - return nil, errors.New("basic_auth_username cannot contain colon or newline") - } - if strings.ContainsAny(input.BasicAuthPassword, "\r\n") { - return nil, errors.New("basic_auth_password cannot contain newline") - } } else { input.BasicAuthUsername = "" input.BasicAuthPassword = ""