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