package validators import ( "net/url" "regexp" "strings" ) const ( schemeHTTP = "http" schemeHTTPS = "https" ) var ( // Regular expressions for validating repository URLs // These regex patterns ensure the URL is in the format of a valid GitHub or GitLab repository // For example: // - GitHub: https://github.com/user/repo githubURLRegex = regexp.MustCompile(`^https?://(www\.)?github\.com/[\w.-]+/[\w.-]+/?$`) gitlabURLRegex = regexp.MustCompile(`^https?://(www\.)?gitlab\.com/[\w.-]+/[\w.-]+/?$`) ) // IsValidRepositoryURL checks if the given URL is valid for the specified repository source func IsValidRepositoryURL(source RepositorySource, url string) bool { switch source { case SourceGitHub: return githubURLRegex.MatchString(url) case SourceGitLab: return gitlabURLRegex.MatchString(url) } return false } // HasNoSpaces checks if a string contains no spaces func HasNoSpaces(s string) bool { return !strings.Contains(s, " ") } // extractTemplateVariables extracts template variables from a URL string // e.g., "http://{host}:{port}/mcp" returns ["host", "port"] func extractTemplateVariables(url string) []string { re := regexp.MustCompile(`\{([^}]+)\}`) matches := re.FindAllStringSubmatch(url, -1) var variables []string for _, match := range matches { if len(match) > 1 { variables = append(variables, match[1]) } } return variables } // replaceTemplateVariables replaces template variables with placeholder values for URL validation func replaceTemplateVariables(rawURL string) string { // Replace common template variables with valid placeholder values for parsing templateReplacements := map[string]string{ "{host}": "example.com", "{port}": "8080", "{path}": "api", "{protocol}": schemeHTTP, "{scheme}": schemeHTTP, } result := rawURL for placeholder, replacement := range templateReplacements { result = strings.ReplaceAll(result, placeholder, replacement) } // Handle any remaining {variable} patterns with context-appropriate placeholders // If the variable is in a port position (after a colon in the host), use a numeric placeholder // Pattern: :/{variable} or :{variable}/ or :{variable} at end portRe := regexp.MustCompile(`:(\{[^}]+\})(/|$)`) result = portRe.ReplaceAllString(result, ":8080$2") // Replace any other remaining {variable} patterns with generic placeholder re := regexp.MustCompile(`\{[^}]+\}`) result = re.ReplaceAllString(result, "placeholder") return result } // IsValidURL checks if a URL is in valid format (basic structure validation) func IsValidURL(rawURL string) bool { // Replace template variables with placeholders for parsing testURL := replaceTemplateVariables(rawURL) // Parse the URL u, err := url.Parse(testURL) if err != nil { return false } // Check if scheme is present (http or https) if u.Scheme != schemeHTTP && u.Scheme != schemeHTTPS { return false } if u.Host == "" { return false } return true } // IsValidSubfolderPath checks if a subfolder path is valid func IsValidSubfolderPath(path string) bool { // Empty path is valid (subfolder is optional) if path == "" { return true } // Must not start with / (must be relative) if strings.HasPrefix(path, "/") { return false } // Must not end with / (clean path format) if strings.HasSuffix(path, "/") { return false } // Check for valid path characters (alphanumeric, dash, underscore, dot, forward slash) validPathRegex := regexp.MustCompile(`^[a-zA-Z0-9\-_./]+$`) if !validPathRegex.MatchString(path) { return false } // Check that path segments are valid segments := strings.Split(path, "/") for _, segment := range segments { // Disallow empty segments ("//"), current dir ("."), and parent dir ("..") if segment == "" || segment == "." || segment == ".." { return false } } return true } // IsValidRemoteURL checks if a URL is valid for remotes (stricter than packages - no localhost allowed) func IsValidRemoteURL(rawURL string) bool { // First check basic URL structure if !IsValidURL(rawURL) { return false } // Replace template variables with placeholders before parsing for localhost check testURL := replaceTemplateVariables(rawURL) // Parse the URL to check for localhost restriction u, err := url.Parse(testURL) if err != nil { return false } // Reject localhost URLs for remotes (security/production concerns) hostname := u.Hostname() if hostname == "localhost" || hostname == "127.0.0.1" || strings.HasSuffix(hostname, ".localhost") { return false } if u.Scheme != schemeHTTPS { return false } return true } // IsValidTemplatedURL validates a URL with template variables against available variables // For packages: validates that template variables reference package arguments or environment variables // For remotes: validates that template variables reference the transport's variables map func IsValidTemplatedURL(rawURL string, availableVariables []string) bool { // First check basic URL structure if !IsValidURL(rawURL) { return false } // Extract template variables from URL templateVars := extractTemplateVariables(rawURL) // If no templates are found, it's a valid static URL if len(templateVars) == 0 { return true } // Validate that all template variables are available availableSet := make(map[string]bool) for _, v := range availableVariables { availableSet[v] = true } for _, templateVar := range templateVars { if !availableSet[templateVar] { return false } } return true }