File size: 3,395 Bytes
e67ab0e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
/**
 * URL validation and sanitization utilities for MCP integration
 */

import { browser } from "$app/environment";
import { dev } from "$app/environment";

/**
 * Sanitize and validate a URL for MCP server connections
 * @param urlString - The URL string to validate
 * @returns Sanitized URL string or null if invalid
 */
export function validateMcpServerUrl(urlString: string): string | null {
	if (!urlString || typeof urlString !== "string") {
		return null;
	}

	try {
		const url = new URL(urlString.trim());

		// Allow http/https only
		if (!["http:", "https:"].includes(url.protocol)) {
			return null;
		}

		// Warn about non-HTTPS in production
		if (!dev && url.protocol === "http:" && browser) {
			console.warn(
				"Warning: Connecting to non-HTTPS MCP server in production. This may expose sensitive data."
			);
		}

		// Block certain localhost/private IPs in production
		if (!dev && isPrivateOrLocalhost(url.hostname)) {
			console.warn("Warning: Localhost/private IP addresses are not recommended in production.");
		}

		return url.toString();
	} catch (error) {
		// Invalid URL
		return null;
	}
}

/**
 * Check if hostname is localhost or a private IP
 */
function isPrivateOrLocalhost(hostname: string): boolean {
	// Localhost checks
	if (
		hostname === "localhost" ||
		hostname === "127.0.0.1" ||
		hostname === "::1" ||
		hostname.endsWith(".localhost")
	) {
		return true;
	}

	// Private IP ranges (IPv4)
	const ipv4Regex = /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|0\.0\.0\.0|169\.254\.)/;
	if (ipv4Regex.test(hostname)) {
		return true;
	}

	return false;
}

/**
 * Sanitize URL by removing sensitive parts
 * Used for logging and display purposes
 */
export function sanitizeUrlForDisplay(urlString: string): string {
	try {
		const url = new URL(urlString);
		// Remove username/password if present
		url.username = "";
		url.password = "";
		return url.toString();
	} catch {
		return urlString;
	}
}

/**
 * Check if URL is safe to connect to
 * Returns an error message if unsafe, null if safe
 */
export function checkUrlSafety(urlString: string): string | null {
	const validated = validateMcpServerUrl(urlString);
	if (!validated) {
		return "Invalid URL. Please use http:// or https:// URLs only.";
	}

	try {
		const url = new URL(validated);

		// Additional safety checks
		if (!dev && url.protocol === "http:") {
			return "Non-HTTPS URLs are not recommended in production. Please use https:// for security.";
		}

		return null; // Safe
	} catch {
		return "Invalid URL format.";
	}
}

/**
 * Check if a header key is likely to contain sensitive data
 */
export function isSensitiveHeader(key: string): boolean {
	const sensitiveKeys = [
		"authorization",
		"api-key",
		"api_key",
		"apikey",
		"token",
		"secret",
		"password",
		"bearer",
		"x-api-key",
		"x-auth-token",
	];

	const lowerKey = key.toLowerCase();
	return sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive));
}

/**
 * Validate header key-value pair
 * Returns error message if invalid, null if valid
 */
export function validateHeader(key: string, value: string): string | null {
	if (!key || !key.trim()) {
		return "Header name is required";
	}

	if (!/^[a-zA-Z0-9_-]+$/.test(key)) {
		return "Header name can only contain letters, numbers, hyphens, and underscores";
	}

	if (!value) {
		return "Header value is required";
	}

	return null;
}