1 /*
2  * Copyright (C) the libgit2 contributors. All rights reserved.
3  *
4  * This file is part of libgit2, distributed under the GNU GPL v2 with
5  * a Linking Exception. For full terms see the included COPYING file.
6  */
7 
8 #include "net.h"
9 #include "netops.h"
10 
11 #include <ctype.h>
12 #include "git2/errors.h"
13 
14 #include "posix.h"
15 #include "buffer.h"
16 #include "http_parser.h"
17 #include "runtime.h"
18 
19 #define DEFAULT_PORT_HTTP  "80"
20 #define DEFAULT_PORT_HTTPS "443"
21 #define DEFAULT_PORT_GIT   "9418"
22 #define DEFAULT_PORT_SSH   "22"
23 
default_port_for_scheme(const char * scheme)24 static const char *default_port_for_scheme(const char *scheme)
25 {
26 	if (strcmp(scheme, "http") == 0)
27 		return DEFAULT_PORT_HTTP;
28 	else if (strcmp(scheme, "https") == 0)
29 		return DEFAULT_PORT_HTTPS;
30 	else if (strcmp(scheme, "git") == 0)
31 		return DEFAULT_PORT_GIT;
32 	else if (strcmp(scheme, "ssh") == 0)
33 		return DEFAULT_PORT_SSH;
34 
35 	return NULL;
36 }
37 
git_net_url_dup(git_net_url * out,git_net_url * in)38 int git_net_url_dup(git_net_url *out, git_net_url *in)
39 {
40 	if (in->scheme) {
41 		out->scheme = git__strdup(in->scheme);
42 		GIT_ERROR_CHECK_ALLOC(out->scheme);
43 	}
44 
45 	if (in->host) {
46 		out->host = git__strdup(in->host);
47 		GIT_ERROR_CHECK_ALLOC(out->host);
48 	}
49 
50 	if (in->port) {
51 		out->port = git__strdup(in->port);
52 		GIT_ERROR_CHECK_ALLOC(out->port);
53 	}
54 
55 	if (in->path) {
56 		out->path = git__strdup(in->path);
57 		GIT_ERROR_CHECK_ALLOC(out->path);
58 	}
59 
60 	if (in->query) {
61 		out->query = git__strdup(in->query);
62 		GIT_ERROR_CHECK_ALLOC(out->query);
63 	}
64 
65 	if (in->username) {
66 		out->username = git__strdup(in->username);
67 		GIT_ERROR_CHECK_ALLOC(out->username);
68 	}
69 
70 	if (in->password) {
71 		out->password = git__strdup(in->password);
72 		GIT_ERROR_CHECK_ALLOC(out->password);
73 	}
74 
75 	return 0;
76 }
77 
git_net_url_parse(git_net_url * url,const char * given)78 int git_net_url_parse(git_net_url *url, const char *given)
79 {
80 	struct http_parser_url u = {0};
81 	bool has_scheme, has_host, has_port, has_path, has_query, has_userinfo;
82 	git_buf scheme = GIT_BUF_INIT,
83 		host = GIT_BUF_INIT,
84 		port = GIT_BUF_INIT,
85 		path = GIT_BUF_INIT,
86 		username = GIT_BUF_INIT,
87 		password = GIT_BUF_INIT,
88 		query = GIT_BUF_INIT;
89 	int error = GIT_EINVALIDSPEC;
90 
91 	if (http_parser_parse_url(given, strlen(given), false, &u)) {
92 		git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given);
93 		goto done;
94 	}
95 
96 	has_scheme = !!(u.field_set & (1 << UF_SCHEMA));
97 	has_host = !!(u.field_set & (1 << UF_HOST));
98 	has_port = !!(u.field_set & (1 << UF_PORT));
99 	has_path = !!(u.field_set & (1 << UF_PATH));
100 	has_query = !!(u.field_set & (1 << UF_QUERY));
101 	has_userinfo = !!(u.field_set & (1 << UF_USERINFO));
102 
103 	if (has_scheme) {
104 		const char *url_scheme = given + u.field_data[UF_SCHEMA].off;
105 		size_t url_scheme_len = u.field_data[UF_SCHEMA].len;
106 		git_buf_put(&scheme, url_scheme, url_scheme_len);
107 		git__strntolower(scheme.ptr, scheme.size);
108 	} else {
109 		git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given);
110 		goto done;
111 	}
112 
113 	if (has_host) {
114 		const char *url_host = given + u.field_data[UF_HOST].off;
115 		size_t url_host_len = u.field_data[UF_HOST].len;
116 		git_buf_decode_percent(&host, url_host, url_host_len);
117 	}
118 
119 	if (has_port) {
120 		const char *url_port = given + u.field_data[UF_PORT].off;
121 		size_t url_port_len = u.field_data[UF_PORT].len;
122 		git_buf_put(&port, url_port, url_port_len);
123 	} else {
124 		const char *default_port = default_port_for_scheme(scheme.ptr);
125 
126 		if (default_port == NULL) {
127 			git_error_set(GIT_ERROR_NET, "unknown scheme for URL '%s'", given);
128 			goto done;
129 		}
130 
131 		git_buf_puts(&port, default_port);
132 	}
133 
134 	if (has_path) {
135 		const char *url_path = given + u.field_data[UF_PATH].off;
136 		size_t url_path_len = u.field_data[UF_PATH].len;
137 		git_buf_put(&path, url_path, url_path_len);
138 	} else {
139 		git_buf_puts(&path, "/");
140 	}
141 
142 	if (has_query) {
143 		const char *url_query = given + u.field_data[UF_QUERY].off;
144 		size_t url_query_len = u.field_data[UF_QUERY].len;
145 		git_buf_decode_percent(&query, url_query, url_query_len);
146 	}
147 
148 	if (has_userinfo) {
149 		const char *url_userinfo = given + u.field_data[UF_USERINFO].off;
150 		size_t url_userinfo_len = u.field_data[UF_USERINFO].len;
151 		const char *colon = memchr(url_userinfo, ':', url_userinfo_len);
152 
153 		if (colon) {
154 			const char *url_username = url_userinfo;
155 			size_t url_username_len = colon - url_userinfo;
156 			const char *url_password = colon + 1;
157 			size_t url_password_len = url_userinfo_len - (url_username_len + 1);
158 
159 			git_buf_decode_percent(&username, url_username, url_username_len);
160 			git_buf_decode_percent(&password, url_password, url_password_len);
161 		} else {
162 			git_buf_decode_percent(&username, url_userinfo, url_userinfo_len);
163 		}
164 	}
165 
166 	if (git_buf_oom(&scheme) ||
167 	    git_buf_oom(&host) ||
168 	    git_buf_oom(&port) ||
169 	    git_buf_oom(&path) ||
170 	    git_buf_oom(&query) ||
171 	    git_buf_oom(&username) ||
172 	    git_buf_oom(&password))
173 		return -1;
174 
175 	url->scheme = git_buf_detach(&scheme);
176 	url->host = git_buf_detach(&host);
177 	url->port = git_buf_detach(&port);
178 	url->path = git_buf_detach(&path);
179 	url->query = git_buf_detach(&query);
180 	url->username = git_buf_detach(&username);
181 	url->password = git_buf_detach(&password);
182 
183 	error = 0;
184 
185 done:
186 	git_buf_dispose(&scheme);
187 	git_buf_dispose(&host);
188 	git_buf_dispose(&port);
189 	git_buf_dispose(&path);
190 	git_buf_dispose(&query);
191 	git_buf_dispose(&username);
192 	git_buf_dispose(&password);
193 	return error;
194 }
195 
git_net_url_joinpath(git_net_url * out,git_net_url * one,const char * two)196 int git_net_url_joinpath(
197 	git_net_url *out,
198 	git_net_url *one,
199 	const char *two)
200 {
201 	git_buf path = GIT_BUF_INIT;
202 	const char *query;
203 	size_t one_len, two_len;
204 
205 	git_net_url_dispose(out);
206 
207 	if ((query = strchr(two, '?')) != NULL) {
208 		two_len = query - two;
209 
210 		if (*(++query) != '\0') {
211 			out->query = git__strdup(query);
212 			GIT_ERROR_CHECK_ALLOC(out->query);
213 		}
214 	} else {
215 		two_len = strlen(two);
216 	}
217 
218 	/* Strip all trailing `/`s from the first path */
219 	one_len = one->path ? strlen(one->path) : 0;
220 	while (one_len && one->path[one_len - 1] == '/')
221 		one_len--;
222 
223 	/* Strip all leading `/`s from the second path */
224 	while (*two == '/') {
225 		two++;
226 		two_len--;
227 	}
228 
229 	git_buf_put(&path, one->path, one_len);
230 	git_buf_putc(&path, '/');
231 	git_buf_put(&path, two, two_len);
232 
233 	if (git_buf_oom(&path))
234 		return -1;
235 
236 	out->path = git_buf_detach(&path);
237 
238 	if (one->scheme) {
239 		out->scheme = git__strdup(one->scheme);
240 		GIT_ERROR_CHECK_ALLOC(out->scheme);
241 	}
242 
243 	if (one->host) {
244 		out->host = git__strdup(one->host);
245 		GIT_ERROR_CHECK_ALLOC(out->host);
246 	}
247 
248 	if (one->port) {
249 		out->port = git__strdup(one->port);
250 		GIT_ERROR_CHECK_ALLOC(out->port);
251 	}
252 
253 	if (one->username) {
254 		out->username = git__strdup(one->username);
255 		GIT_ERROR_CHECK_ALLOC(out->username);
256 	}
257 
258 	if (one->password) {
259 		out->password = git__strdup(one->password);
260 		GIT_ERROR_CHECK_ALLOC(out->password);
261 	}
262 
263 	return 0;
264 }
265 
266 /*
267  * Some servers strip the query parameters from the Location header
268  * when sending a redirect. Others leave it in place.
269  * Check for both, starting with the stripped case first,
270  * since it appears to be more common.
271  */
remove_service_suffix(git_net_url * url,const char * service_suffix)272 static void remove_service_suffix(
273 	git_net_url *url,
274 	const char *service_suffix)
275 {
276 	const char *service_query = strchr(service_suffix, '?');
277 	size_t full_suffix_len = strlen(service_suffix);
278 	size_t suffix_len = service_query ?
279 		(size_t)(service_query - service_suffix) : full_suffix_len;
280 	size_t path_len = strlen(url->path);
281 	ssize_t truncate = -1;
282 
283 	/*
284 	 * Check for a redirect without query parameters,
285 	 * like "/newloc/info/refs"'
286 	 */
287 	if (suffix_len && path_len >= suffix_len) {
288 		size_t suffix_offset = path_len - suffix_len;
289 
290 		if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 &&
291 		    (!service_query || git__strcmp(url->query, service_query + 1) == 0)) {
292 			truncate = suffix_offset;
293 		}
294 	}
295 
296 	/*
297 	 * If we haven't already found where to truncate to remove the
298 	 * suffix, check for a redirect with query parameters, like
299 	 * "/newloc/info/refs?service=git-upload-pack"
300 	 */
301 	if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0)
302 		truncate = path_len - full_suffix_len;
303 
304 	/* Ensure we leave a minimum of '/' as the path */
305 	if (truncate == 0)
306 		truncate++;
307 
308 	if (truncate > 0) {
309 		url->path[truncate] = '\0';
310 
311 		git__free(url->query);
312 		url->query = NULL;
313 	}
314 }
315 
git_net_url_apply_redirect(git_net_url * url,const char * redirect_location,const char * service_suffix)316 int git_net_url_apply_redirect(
317 	git_net_url *url,
318 	const char *redirect_location,
319 	const char *service_suffix)
320 {
321 	git_net_url tmp = GIT_NET_URL_INIT;
322 	int error = 0;
323 
324 	GIT_ASSERT(url);
325 	GIT_ASSERT(redirect_location);
326 
327 	if (redirect_location[0] == '/') {
328 		git__free(url->path);
329 
330 		if ((url->path = git__strdup(redirect_location)) == NULL) {
331 			error = -1;
332 			goto done;
333 		}
334 	} else {
335 		git_net_url *original = url;
336 
337 		if ((error = git_net_url_parse(&tmp, redirect_location)) < 0)
338 			goto done;
339 
340 		/* Validate that this is a legal redirection */
341 
342 		if (original->scheme &&
343 			strcmp(original->scheme, tmp.scheme) != 0 &&
344 			strcmp(tmp.scheme, "https") != 0) {
345 			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
346 				original->scheme, tmp.scheme);
347 
348 			error = -1;
349 			goto done;
350 		}
351 
352 		if (original->host &&
353 		    git__strcasecmp(original->host, tmp.host) != 0) {
354 			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
355 				original->host, tmp.host);
356 
357 			error = -1;
358 			goto done;
359 		}
360 
361 		git_net_url_swap(url, &tmp);
362 	}
363 
364 	/* Remove the service suffix if it was given to us */
365 	if (service_suffix)
366 		remove_service_suffix(url, service_suffix);
367 
368 done:
369 	git_net_url_dispose(&tmp);
370 	return error;
371 }
372 
git_net_url_valid(git_net_url * url)373 bool git_net_url_valid(git_net_url *url)
374 {
375 	return (url->host && url->port && url->path);
376 }
377 
git_net_url_is_default_port(git_net_url * url)378 bool git_net_url_is_default_port(git_net_url *url)
379 {
380 	const char *default_port;
381 
382 	if ((default_port = default_port_for_scheme(url->scheme)) != NULL)
383 		return (strcmp(url->port, default_port) == 0);
384 	else
385 		return false;
386 }
387 
git_net_url_is_ipv6(git_net_url * url)388 bool git_net_url_is_ipv6(git_net_url *url)
389 {
390 	return (strchr(url->host, ':') != NULL);
391 }
392 
git_net_url_swap(git_net_url * a,git_net_url * b)393 void git_net_url_swap(git_net_url *a, git_net_url *b)
394 {
395 	git_net_url tmp = GIT_NET_URL_INIT;
396 
397 	memcpy(&tmp, a, sizeof(git_net_url));
398 	memcpy(a, b, sizeof(git_net_url));
399 	memcpy(b, &tmp, sizeof(git_net_url));
400 }
401 
git_net_url_fmt(git_buf * buf,git_net_url * url)402 int git_net_url_fmt(git_buf *buf, git_net_url *url)
403 {
404 	GIT_ASSERT_ARG(url);
405 	GIT_ASSERT_ARG(url->scheme);
406 	GIT_ASSERT_ARG(url->host);
407 
408 	git_buf_puts(buf, url->scheme);
409 	git_buf_puts(buf, "://");
410 
411 	if (url->username) {
412 		git_buf_puts(buf, url->username);
413 
414 		if (url->password) {
415 			git_buf_puts(buf, ":");
416 			git_buf_puts(buf, url->password);
417 		}
418 
419 		git_buf_putc(buf, '@');
420 	}
421 
422 	git_buf_puts(buf, url->host);
423 
424 	if (url->port && !git_net_url_is_default_port(url)) {
425 		git_buf_putc(buf, ':');
426 		git_buf_puts(buf, url->port);
427 	}
428 
429 	git_buf_puts(buf, url->path ? url->path : "/");
430 
431 	if (url->query) {
432 		git_buf_putc(buf, '?');
433 		git_buf_puts(buf, url->query);
434 	}
435 
436 	return git_buf_oom(buf) ? -1 : 0;
437 }
438 
git_net_url_fmt_path(git_buf * buf,git_net_url * url)439 int git_net_url_fmt_path(git_buf *buf, git_net_url *url)
440 {
441 	git_buf_puts(buf, url->path ? url->path : "/");
442 
443 	if (url->query) {
444 		git_buf_putc(buf, '?');
445 		git_buf_puts(buf, url->query);
446 	}
447 
448 	return git_buf_oom(buf) ? -1 : 0;
449 }
450 
matches_pattern(git_net_url * url,const char * pattern,size_t pattern_len)451 static bool matches_pattern(
452 	git_net_url *url,
453 	const char *pattern,
454 	size_t pattern_len)
455 {
456 	const char *domain, *port = NULL, *colon;
457 	size_t host_len, domain_len, port_len = 0, wildcard = 0;
458 
459 	GIT_UNUSED(url);
460 	GIT_UNUSED(pattern);
461 
462 	if (!pattern_len)
463 		return false;
464 	else if (pattern_len == 1 && pattern[0] == '*')
465 		return true;
466 	else if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.')
467 		wildcard = 2;
468 	else if (pattern[0] == '.')
469 		wildcard = 1;
470 
471 	domain = pattern + wildcard;
472 	domain_len = pattern_len - wildcard;
473 
474 	if ((colon = memchr(domain, ':', domain_len)) != NULL) {
475 		domain_len = colon - domain;
476 		port = colon + 1;
477 		port_len = pattern_len - wildcard - domain_len - 1;
478 	}
479 
480 	/* A pattern's port *must* match if it's specified */
481 	if (port_len && git__strlcmp(url->port, port, port_len) != 0)
482 		return false;
483 
484 	/* No wildcard?  Host must match exactly. */
485 	if (!wildcard)
486 		return !git__strlcmp(url->host, domain, domain_len);
487 
488 	/* Wildcard: ensure there's (at least) a suffix match */
489 	if ((host_len = strlen(url->host)) < domain_len ||
490 	    memcmp(url->host + (host_len - domain_len), domain, domain_len))
491 		return false;
492 
493 	/* The pattern is *.domain and the host is simply domain */
494 	if (host_len == domain_len)
495 		return true;
496 
497 	/* The pattern is *.domain and the host is foo.domain */
498 	return (url->host[host_len - domain_len - 1] == '.');
499 }
500 
git_net_url_matches_pattern(git_net_url * url,const char * pattern)501 bool git_net_url_matches_pattern(git_net_url *url, const char *pattern)
502 {
503 	return matches_pattern(url, pattern, strlen(pattern));
504 }
505 
git_net_url_matches_pattern_list(git_net_url * url,const char * pattern_list)506 bool git_net_url_matches_pattern_list(
507 	git_net_url *url,
508 	const char *pattern_list)
509 {
510 	const char *pattern, *pattern_end, *sep;
511 
512 	for (pattern = pattern_list;
513 	     pattern && *pattern;
514 	     pattern = sep ? sep + 1 : NULL) {
515 		sep = strchr(pattern, ',');
516 		pattern_end = sep ? sep : strchr(pattern, '\0');
517 
518 		if (matches_pattern(url, pattern, (pattern_end - pattern)))
519 			return true;
520 	}
521 
522 	return false;
523 }
524 
git_net_url_dispose(git_net_url * url)525 void git_net_url_dispose(git_net_url *url)
526 {
527 	if (url->username)
528 		git__memzero(url->username, strlen(url->username));
529 
530 	if (url->password)
531 		git__memzero(url->password, strlen(url->password));
532 
533 	git__free(url->scheme); url->scheme = NULL;
534 	git__free(url->host); url->host = NULL;
535 	git__free(url->port); url->port = NULL;
536 	git__free(url->path); url->path = NULL;
537 	git__free(url->query); url->query = NULL;
538 	git__free(url->username); url->username = NULL;
539 	git__free(url->password); url->password = NULL;
540 }
541