Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions services/core/cmd/server/daemon_bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,26 @@ func TestBootstrapAddressUsesPublicOrigin(t *testing.T) {
})
}
}

func TestRuntimeWebSocketURL(t *testing.T) {
for _, c := range []struct {
coreURL string
want string
}{
{"https://core.example", "wss://core.example/api/v1/agent-daemon/ws"},
{"https://core.example:8443", "wss://core.example:8443/api/v1/agent-daemon/ws"},
{"http://127.0.0.1:8091", "ws://127.0.0.1:8091/api/v1/agent-daemon/ws"},
{"http://10.0.0.5:8080", "ws://10.0.0.5:8080/api/v1/agent-daemon/ws"},
{"http://[2001:db8::1]:8080", "ws://[2001:db8::1]:8080/api/v1/agent-daemon/ws"},
} {
got, err := runtimeWebSocketURL(c.coreURL)
if err != nil || got != c.want {
t.Errorf("runtimeWebSocketURL(%q) = %q, %v; want %q", c.coreURL, got, err, c.want)
}
}
for _, coreURL := range []string{"ftp://core.example", "core.example", "https://user:secret@core.example"} {
if _, err := runtimeWebSocketURL(coreURL); err == nil {
t.Errorf("runtimeWebSocketURL(%q) accepted", coreURL)
}
}
}
42 changes: 42 additions & 0 deletions services/core/cmd/server/process_configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,45 @@ func TestPublicURLMustBeACanonicalOrigin(t *testing.T) {
}
}
}

func TestPublicURLDrivesInsecureWebSocketURL(t *testing.T) {
const insecure = "http://10.0.0.5:8080"
t.Setenv("OAC_PUBLIC_URL", insecure)
if _, err := processconfig.PublicURL(); err == nil {
t.Fatal("accepted a non-loopback HTTP origin with OAC_ALLOW_INSECURE_ORIGIN unset")
}
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
public, err := processconfig.PublicURL()
if err != nil || public != insecure {
t.Fatalf("PublicURL() = %q, %v", public, err)
}
if ws, err := runtimeWebSocketURL(public); err != nil || ws != "ws://10.0.0.5:8080/api/v1/agent-daemon/ws" {
t.Fatalf("runtimeWebSocketURL(%q) = %q, %v", public, ws, err)
}
}

// installationFacts backs GET /core/v1/installation; the switch must reach the
// settings snapshot the console reads.
func TestInstallationFactsReportAllowInsecureOrigin(t *testing.T) {
t.Setenv("OAC_PUBLIC_URL", "http://10.0.0.5:8080")
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
facts, err := installationFacts("http://10.0.0.5:8080")
if err != nil {
t.Fatal(err)
}
if facts.Configuration == nil {
t.Fatal("installation facts omitted the settings snapshot")
}
found := false
for _, setting := range facts.Configuration.Settings {
if setting.Key == "allow_insecure_origin" {
found = true
if setting.Value != true || setting.Default != false || setting.Sensitive {
t.Fatalf("allow_insecure_origin = %+v", setting)
}
}
}
if !found {
t.Fatal("installation facts omitted allow_insecure_origin")
}
}
14 changes: 13 additions & 1 deletion services/core/internal/deployment/public_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import (
// credential. Plain HTTP is reserved for explicit loopback development hosts.
// OAC_PUBLIC_URL must pass it.
func ValidateCoreURL(value string) error {
return validateCoreURL(value, false)
}

// ValidateCoreURLAllowingInsecure accepts every origin ValidateCoreURL accepts
// and, in addition, a non-loopback plain HTTP origin. Core selects it only when
// OAC_ALLOW_INSECURE_ORIGIN is set, for development and testing installations
// where credentials and API keys then travel in plaintext.
func ValidateCoreURLAllowingInsecure(value string) error {
return validateCoreURL(value, true)
}

func validateCoreURL(value string, allowInsecure bool) error {
u, err := url.Parse(value)
if err != nil || u.Hostname() == "" || u.User != nil || u.Path != "" || u.RawPath != "" || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.RawFragment != "" || u.Opaque != "" || u.String() != value || u.Host != strings.ToLower(u.Host) {
return ErrInvalidInput
Expand Down Expand Up @@ -43,7 +55,7 @@ func ValidateCoreURL(value string) error {
}
}
}
if u.Scheme != "https" && !(u.Scheme == "http" && loopback) {
if u.Scheme != "https" && !(u.Scheme == "http" && (loopback || allowInsecure)) {
return ErrInvalidInput
}
return nil
Expand Down
26 changes: 26 additions & 0 deletions services/core/internal/deployment/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ func TestValidateCoreURL(t *testing.T) {
}
}

// ValidateCoreURLAllowingInsecure adds a non-loopback HTTP origin without
// weakening the default ValidateCoreURL contract the deployment side shares.
func TestValidateCoreURLAllowingInsecure(t *testing.T) {
for _, value := range []string{
"https://core.example", "https://core.example:8443", "http://localhost:8091", "http://127.0.0.2:8091",
"http://[::1]:8091", "https://[2001:db8::1]", "http://core.example", "http://core.example:8091",
"http://192.168.1.10:8080", "http://10.0.0.5", "http://[2001:db8::1]:8080",
} {
if err := ValidateCoreURLAllowingInsecure(value); err != nil {
t.Errorf("insecure Core URL %q rejected: %v", value, err)
}
}
for _, value := range []string{
"", "ftp://core.example", "ws://core.example", "http://core.example/", "https://core.example/path",
"https://user:secret@core.example", "http://CORE.example", "http://core.example:0080", "http://core.example.",
"http://core..example", "http://core_example", "http://[not-an-ip]",
} {
if err := ValidateCoreURLAllowingInsecure(value); !errors.Is(err, ErrInvalidInput) {
t.Errorf("invalid insecure Core URL %q accepted: %v", value, err)
}
}
if err := ValidateCoreURL("http://core.example"); !errors.Is(err, ErrInvalidInput) {
t.Errorf("ValidateCoreURL accepted a non-loopback HTTP origin: %v", err)
}
}

func TestParseID(t *testing.T) {
id := uuid.New()
if got, err := parseID(strings.ToUpper(id.String())); err != nil || got != id.String() {
Expand Down
15 changes: 15 additions & 0 deletions services/core/internal/processconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func Settings() ([]api.InstallationSetting, error) {
}
return []api.InstallationSetting{
setting("public_url", publicValue, nil, true, []string{"core", "web"}),
setting("allow_insecure_origin", allowInsecureOrigin(), false, true, []string{"core"}),
setting("log.level", level, "info", true, []string{"core", "web"}),
setting("log.format", format, "auto", true, []string{"core", "web"}),
setting("log.add_source", addSource, false, true, []string{"core", "web"}),
Expand Down Expand Up @@ -106,12 +107,26 @@ func PublicURL() (string, error) {
if value == "" {
return "", nil
}
if allowInsecureOrigin() {
if deployment.ValidateCoreURLAllowingInsecure(value) != nil {
return "", configErr("OAC_PUBLIC_URL must be a canonical origin without path, credentials, query or fragment, such as https://core.example; plain HTTP is accepted because OAC_ALLOW_INSECURE_ORIGIN is set")
}
return value, nil
}
if deployment.ValidateCoreURL(value) != nil {
return "", configErr("OAC_PUBLIC_URL must be a canonical HTTPS origin without path, credentials, query or fragment, such as https://core.example; plain HTTP is accepted only for a loopback host")
}
return value, nil
}

// allowInsecureOrigin reads OAC_ALLOW_INSECURE_ORIGIN, the single switch that
// permits a non-loopback plain HTTP public origin for development and testing.
// The deployment side owns the value; only "1" enables it, and Core never
// infers it from OAC_PUBLIC_URL or reads another variable.
func allowInsecureOrigin() bool {
return os.Getenv("OAC_ALLOW_INSECURE_ORIGIN") == "1"
}

// InstallationID reads the file named by OAC_INSTALLATION_ID_FILE. The ID
// enables the sandbox deployment and node routes; unset leaves them off.
func InstallationID() (string, error) {
Expand Down
62 changes: 62 additions & 0 deletions services/core/internal/processconfig/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package processconfig

import (
"os"
"strings"
"testing"
)
Expand Down Expand Up @@ -47,6 +48,67 @@ func TestSettingsReportEffectiveValuesAndHideHistory(t *testing.T) {
}
}

func TestPublicURLAcceptsInsecureOriginOnlyWhenEnabled(t *testing.T) {
const insecure = "http://10.0.0.5:8080"
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
if err := os.Unsetenv("OAC_ALLOW_INSECURE_ORIGIN"); err != nil {
t.Fatal(err)
}
t.Setenv("OAC_PUBLIC_URL", insecure)
if _, err := PublicURL(); err == nil {
t.Fatal("accepted a non-loopback HTTP origin with OAC_ALLOW_INSECURE_ORIGIN unset")
} else if err.Error() != "OAC_PUBLIC_URL must be a canonical HTTPS origin without path, credentials, query or fragment, such as https://core.example; plain HTTP is accepted only for a loopback host" {
t.Fatalf("switch-off error text changed: %v", err)
}
// Only the deployment side's derived value "1" enables the relaxed check.
for _, value := range []string{"0", "true", "yes", "TRUE", "2"} {
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", value)
if _, err := PublicURL(); err == nil {
t.Fatalf("accepted a non-loopback HTTP origin with OAC_ALLOW_INSECURE_ORIGIN=%q", value)
}
}
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
got, err := PublicURL()
if err != nil || got != insecure {
t.Fatalf("PublicURL() = %q, %v", got, err)
}
// A malformed origin is still rejected while the switch is on.
t.Setenv("OAC_PUBLIC_URL", "http://Core.example")
if _, err := PublicURL(); err == nil {
t.Fatal("accepted a non-canonical origin with OAC_ALLOW_INSECURE_ORIGIN set")
}
}

func TestSettingsReportAllowInsecureOrigin(t *testing.T) {
t.Setenv("OAC_PUBLIC_URL", "https://core.example")
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
if err := os.Unsetenv("OAC_ALLOW_INSECURE_ORIGIN"); err != nil {
t.Fatal(err)
}
if value := settingValue(t, "allow_insecure_origin"); value != false {
t.Fatalf("allow_insecure_origin = %v; want false", value)
}
t.Setenv("OAC_ALLOW_INSECURE_ORIGIN", "1")
if value := settingValue(t, "allow_insecure_origin"); value != true {
t.Fatalf("allow_insecure_origin = %v; want true", value)
}
}

func settingValue(t *testing.T, key string) any {
t.Helper()
settings, err := Settings()
if err != nil {
t.Fatal(err)
}
for _, setting := range settings {
if setting.Key == key {
return setting.Value
}
}
t.Fatalf("setting %s is missing", key)
return nil
}

func TestHarnessesDefaultToEveryQualifiedHarness(t *testing.T) {
t.Setenv("OAC_DEFAULT_HARNESS", "")
t.Setenv("OAC_HARNESSES", "")
Expand Down
Loading