package config import ( "fmt" "net" "net/url" "strings" ) // NormalizeAllowedOrigin accepts a browser Origin value such as // https://www.example.com and returns a canonical form for exact matching. func NormalizeAllowedOrigin(value string) (string, error) { value = strings.TrimSpace(value) if value == "" { return "", nil } u, err := url.Parse(value) if err != nil || u.Scheme == "" || u.Host == "" { return "", fmt.Errorf("Origin must include scheme and host: %s", value) } scheme := strings.ToLower(u.Scheme) if scheme != "http" && scheme != "https" { return "", fmt.Errorf("Origin scheme must be http or https: %s", value) } if (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.Fragment != "" { return "", fmt.Errorf("Origin must not include path, query, or fragment: %s", value) } host := normalizeOriginHostPort(u.Host, scheme) if host == "" { return "", fmt.Errorf("Origin host is required: %s", value) } return scheme + "://" + host, nil } func NormalizeAllowedOrigins(values []string) ([]string, error) { result := make([]string, 0, len(values)) seen := map[string]bool{} for _, value := range values { origin, err := NormalizeAllowedOrigin(value) if err != nil { return nil, err } if origin == "" || seen[origin] { continue } seen[origin] = true result = append(result, origin) } return result, nil } func IsOriginAllowed(origin string, requestHost string) bool { origin = strings.TrimSpace(origin) if origin == "" { return true } if isSameRequestOrigin(origin, requestHost) { return true } normalized, err := NormalizeAllowedOrigin(origin) if err != nil { return false } if AppConfig == nil { return false } for _, allowed := range AppConfig.WebSSHAllowedOrigins { allowed, err := NormalizeAllowedOrigin(allowed) if err == nil && normalized == allowed { return true } } return false } func isSameRequestOrigin(origin string, requestHost string) bool { u, err := url.Parse(origin) if err != nil || u.Host == "" { return false } originHost := normalizeHostOnly(u.Hostname()) host := normalizeHostOnly(requestHost) if originHost == "" || host == "" { return false } if originHost == host { return true } return isLoopbackHost(originHost) && isLoopbackHost(host) } func normalizeOriginHostPort(raw string, scheme string) string { host := raw port := "" if h, p, err := net.SplitHostPort(raw); err == nil { host = h port = p } host = normalizeHostOnly(host) if host == "" { return "" } if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") { port = "" } if port != "" { return net.JoinHostPort(host, port) } if strings.Contains(host, ":") && net.ParseIP(host) != nil { return "[" + host + "]" } return host } func normalizeHostOnly(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if h, _, err := net.SplitHostPort(raw); err == nil { raw = h } raw = strings.Trim(raw, "[]") if ip := net.ParseIP(raw); ip != nil { return strings.ToLower(ip.String()) } return strings.TrimSuffix(strings.ToLower(raw), ".") } func isLoopbackHost(host string) bool { if host == "localhost" { return true } ip := net.ParseIP(host) return ip != nil && ip.IsLoopback() }