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 "global.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_parse(git_net_url * url,const char * given)38 int git_net_url_parse(git_net_url *url, const char *given)
39 {
40 	struct http_parser_url u = {0};
41 	bool has_scheme, has_host, has_port, has_path, has_query, has_userinfo;
42 	git_buf scheme = GIT_BUF_INIT,
43 		host = GIT_BUF_INIT,
44 		port = GIT_BUF_INIT,
45 		path = GIT_BUF_INIT,
46 		username = GIT_BUF_INIT,
47 		password = GIT_BUF_INIT,
48 		query = GIT_BUF_INIT;
49 	int error = GIT_EINVALIDSPEC;
50 
51 	if (http_parser_parse_url(given, strlen(given), false, &u)) {
52 		git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given);
53 		goto done;
54 	}
55 
56 	has_scheme = !!(u.field_set & (1 << UF_SCHEMA));
57 	has_host = !!(u.field_set & (1 << UF_HOST));
58 	has_port = !!(u.field_set & (1 << UF_PORT));
59 	has_path = !!(u.field_set & (1 << UF_PATH));
60 	has_query = !!(u.field_set & (1 << UF_QUERY));
61 	has_userinfo = !!(u.field_set & (1 << UF_USERINFO));
62 
63 	if (has_scheme) {
64 		const char *url_scheme = given + u.field_data[UF_SCHEMA].off;
65 		size_t url_scheme_len = u.field_data[UF_SCHEMA].len;
66 		git_buf_put(&scheme, url_scheme, url_scheme_len);
67 		git__strntolower(scheme.ptr, scheme.size);
68 	} else {
69 		git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given);
70 		goto done;
71 	}
72 
73 	if (has_host) {
74 		const char *url_host = given + u.field_data[UF_HOST].off;
75 		size_t url_host_len = u.field_data[UF_HOST].len;
76 		git_buf_decode_percent(&host, url_host, url_host_len);
77 	}
78 
79 	if (has_port) {
80 		const char *url_port = given + u.field_data[UF_PORT].off;
81 		size_t url_port_len = u.field_data[UF_PORT].len;
82 		git_buf_put(&port, url_port, url_port_len);
83 	} else {
84 		const char *default_port = default_port_for_scheme(scheme.ptr);
85 
86 		if (default_port == NULL) {
87 			git_error_set(GIT_ERROR_NET, "unknown scheme for URL '%s'", given);
88 			goto done;
89 		}
90 
91 		git_buf_puts(&port, default_port);
92 	}
93 
94 	if (has_path) {
95 		const char *url_path = given + u.field_data[UF_PATH].off;
96 		size_t url_path_len = u.field_data[UF_PATH].len;
97 		git_buf_put(&path, url_path, url_path_len);
98 	} else {
99 		git_buf_puts(&path, "/");
100 	}
101 
102 	if (has_query) {
103 		const char *url_query = given + u.field_data[UF_QUERY].off;
104 		size_t url_query_len = u.field_data[UF_QUERY].len;
105 		git_buf_decode_percent(&query, url_query, url_query_len);
106 	}
107 
108 	if (has_userinfo) {
109 		const char *url_userinfo = given + u.field_data[UF_USERINFO].off;
110 		size_t url_userinfo_len = u.field_data[UF_USERINFO].len;
111 		const char *colon = memchr(url_userinfo, ':', url_userinfo_len);
112 
113 		if (colon) {
114 			const char *url_username = url_userinfo;
115 			size_t url_username_len = colon - url_userinfo;
116 			const char *url_password = colon + 1;
117 			size_t url_password_len = url_userinfo_len - (url_username_len + 1);
118 
119 			git_buf_decode_percent(&username, url_username, url_username_len);
120 			git_buf_decode_percent(&password, url_password, url_password_len);
121 		} else {
122 			git_buf_decode_percent(&username, url_userinfo, url_userinfo_len);
123 		}
124 	}
125 
126 	if (git_buf_oom(&scheme) ||
127 	    git_buf_oom(&host) ||
128 	    git_buf_oom(&port) ||
129 	    git_buf_oom(&path) ||
130 	    git_buf_oom(&query) ||
131 	    git_buf_oom(&username) ||
132 	    git_buf_oom(&password))
133 		return -1;
134 
135 	url->scheme = git_buf_detach(&scheme);
136 	url->host = git_buf_detach(&host);
137 	url->port = git_buf_detach(&port);
138 	url->path = git_buf_detach(&path);
139 	url->query = git_buf_detach(&query);
140 	url->username = git_buf_detach(&username);
141 	url->password = git_buf_detach(&password);
142 
143 	error = 0;
144 
145 done:
146 	git_buf_dispose(&scheme);
147 	git_buf_dispose(&host);
148 	git_buf_dispose(&port);
149 	git_buf_dispose(&path);
150 	git_buf_dispose(&query);
151 	git_buf_dispose(&username);
152 	git_buf_dispose(&password);
153 	return error;
154 }
155 
git_net_url_joinpath(git_net_url * out,git_net_url * one,const char * two)156 int git_net_url_joinpath(
157 	git_net_url *out,
158 	git_net_url *one,
159 	const char *two)
160 {
161 	git_buf path = GIT_BUF_INIT;
162 	const char *query;
163 	size_t one_len, two_len;
164 
165 	git_net_url_dispose(out);
166 
167 	if ((query = strchr(two, '?')) != NULL) {
168 		two_len = query - two;
169 
170 		if (*(++query) != '\0') {
171 			out->query = git__strdup(query);
172 			GIT_ERROR_CHECK_ALLOC(out->query);
173 		}
174 	} else {
175 		two_len = strlen(two);
176 	}
177 
178 	/* Strip all trailing `/`s from the first path */
179 	one_len = one->path ? strlen(one->path) : 0;
180 	while (one_len && one->path[one_len - 1] == '/')
181 		one_len--;
182 
183 	/* Strip all leading `/`s from the second path */
184 	while (*two == '/') {
185 		two++;
186 		two_len--;
187 	}
188 
189 	git_buf_put(&path, one->path, one_len);
190 	git_buf_putc(&path, '/');
191 	git_buf_put(&path, two, two_len);
192 
193 	if (git_buf_oom(&path))
194 		return -1;
195 
196 	out->path = git_buf_detach(&path);
197 
198 	if (one->scheme) {
199 		out->scheme = git__strdup(one->scheme);
200 		GIT_ERROR_CHECK_ALLOC(out->scheme);
201 	}
202 
203 	if (one->host) {
204 		out->host = git__strdup(one->host);
205 		GIT_ERROR_CHECK_ALLOC(out->host);
206 	}
207 
208 	if (one->port) {
209 		out->port = git__strdup(one->port);
210 		GIT_ERROR_CHECK_ALLOC(out->port);
211 	}
212 
213 	if (one->username) {
214 		out->username = git__strdup(one->username);
215 		GIT_ERROR_CHECK_ALLOC(out->username);
216 	}
217 
218 	if (one->password) {
219 		out->password = git__strdup(one->password);
220 		GIT_ERROR_CHECK_ALLOC(out->password);
221 	}
222 
223 	return 0;
224 }
225 
226 /*
227  * Some servers strip the query parameters from the Location header
228  * when sending a redirect. Others leave it in place.
229  * Check for both, starting with the stripped case first,
230  * since it appears to be more common.
231  */
remove_service_suffix(git_net_url * url,const char * service_suffix)232 static void remove_service_suffix(
233 	git_net_url *url,
234 	const char *service_suffix)
235 {
236 	const char *service_query = strchr(service_suffix, '?');
237 	size_t full_suffix_len = strlen(service_suffix);
238 	size_t suffix_len = service_query ?
239 		(size_t)(service_query - service_suffix) : full_suffix_len;
240 	size_t path_len = strlen(url->path);
241 	ssize_t truncate = -1;
242 
243 	/*
244 	 * Check for a redirect without query parameters,
245 	 * like "/newloc/info/refs"'
246 	 */
247 	if (suffix_len && path_len >= suffix_len) {
248 		size_t suffix_offset = path_len - suffix_len;
249 
250 		if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 &&
251 		    (!service_query || git__strcmp(url->query, service_query + 1) == 0)) {
252 			truncate = suffix_offset;
253 		}
254 	}
255 
256 	/*
257 	 * If we haven't already found where to truncate to remove the
258 	 * suffix, check for a redirect with query parameters, like
259 	 * "/newloc/info/refs?service=git-upload-pack"
260 	 */
261 	if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0)
262 		truncate = path_len - full_suffix_len;
263 
264 	/* Ensure we leave a minimum of '/' as the path */
265 	if (truncate == 0)
266 		truncate++;
267 
268 	if (truncate > 0) {
269 		url->path[truncate] = '\0';
270 
271 		git__free(url->query);
272 		url->query = NULL;
273 	}
274 }
275 
git_net_url_apply_redirect(git_net_url * url,const char * redirect_location,const char * service_suffix)276 int git_net_url_apply_redirect(
277 	git_net_url *url,
278 	const char *redirect_location,
279 	const char *service_suffix)
280 {
281 	git_net_url tmp = GIT_NET_URL_INIT;
282 	int error = 0;
283 
284 	assert(url && redirect_location);
285 
286 	if (redirect_location[0] == '/') {
287 		git__free(url->path);
288 
289 		if ((url->path = git__strdup(redirect_location)) == NULL) {
290 			error = -1;
291 			goto done;
292 		}
293 	} else {
294 		git_net_url *original = url;
295 
296 		if ((error = git_net_url_parse(&tmp, redirect_location)) < 0)
297 			goto done;
298 
299 		/* Validate that this is a legal redirection */
300 
301 		if (original->scheme &&
302 			strcmp(original->scheme, tmp.scheme) != 0 &&
303 			strcmp(tmp.scheme, "https") != 0) {
304 			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
305 				original->scheme, tmp.scheme);
306 
307 			error = -1;
308 			goto done;
309 		}
310 
311 		if (original->host &&
312 		    git__strcasecmp(original->host, tmp.host) != 0) {
313 			git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
314 				original->host, tmp.host);
315 
316 			error = -1;
317 			goto done;
318 		}
319 
320 		git_net_url_swap(url, &tmp);
321 	}
322 
323 	/* Remove the service suffix if it was given to us */
324 	if (service_suffix)
325 		remove_service_suffix(url, service_suffix);
326 
327 done:
328 	git_net_url_dispose(&tmp);
329 	return error;
330 }
331 
git_net_url_valid(git_net_url * url)332 bool git_net_url_valid(git_net_url *url)
333 {
334 	return (url->host && url->port && url->path);
335 }
336 
git_net_url_is_default_port(git_net_url * url)337 int git_net_url_is_default_port(git_net_url *url)
338 {
339 	return (strcmp(url->port, default_port_for_scheme(url->scheme)) == 0);
340 }
341 
git_net_url_swap(git_net_url * a,git_net_url * b)342 void git_net_url_swap(git_net_url *a, git_net_url *b)
343 {
344 	git_net_url tmp = GIT_NET_URL_INIT;
345 
346 	memcpy(&tmp, a, sizeof(git_net_url));
347 	memcpy(a, b, sizeof(git_net_url));
348 	memcpy(b, &tmp, sizeof(git_net_url));
349 }
350 
git_net_url_fmt(git_buf * buf,git_net_url * url)351 int git_net_url_fmt(git_buf *buf, git_net_url *url)
352 {
353 	git_buf_puts(buf, url->scheme);
354 	git_buf_puts(buf, "://");
355 
356 	if (url->username) {
357 		git_buf_puts(buf, url->username);
358 
359 		if (url->password) {
360 			git_buf_puts(buf, ":");
361 			git_buf_puts(buf, url->password);
362 		}
363 
364 		git_buf_putc(buf, '@');
365 	}
366 
367 	git_buf_puts(buf, url->host);
368 
369 	if (url->port && !git_net_url_is_default_port(url)) {
370 		git_buf_putc(buf, ':');
371 		git_buf_puts(buf, url->port);
372 	}
373 
374 	git_buf_puts(buf, url->path ? url->path : "/");
375 
376 	if (url->query) {
377 		git_buf_putc(buf, '?');
378 		git_buf_puts(buf, url->query);
379 	}
380 
381 	return git_buf_oom(buf) ? -1 : 0;
382 }
383 
git_net_url_fmt_path(git_buf * buf,git_net_url * url)384 int git_net_url_fmt_path(git_buf *buf, git_net_url *url)
385 {
386 	git_buf_puts(buf, url->path ? url->path : "/");
387 
388 	if (url->query) {
389 		git_buf_putc(buf, '?');
390 		git_buf_puts(buf, url->query);
391 	}
392 
393 	return git_buf_oom(buf) ? -1 : 0;
394 }
395 
git_net_url_dispose(git_net_url * url)396 void git_net_url_dispose(git_net_url *url)
397 {
398 	if (url->username)
399 		git__memzero(url->username, strlen(url->username));
400 
401 	if (url->password)
402 		git__memzero(url->password, strlen(url->password));
403 
404 	git__free(url->scheme); url->scheme = NULL;
405 	git__free(url->host); url->host = NULL;
406 	git__free(url->port); url->port = NULL;
407 	git__free(url->path); url->path = NULL;
408 	git__free(url->query); url->query = NULL;
409 	git__free(url->username); url->username = NULL;
410 	git__free(url->password); url->password = NULL;
411 }
412