/src/libgit2/src/util/net.c
Line | Count | Source |
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 | | |
10 | | #include <ctype.h> |
11 | | |
12 | | #include "posix.h" |
13 | | #include "str.h" |
14 | | #include "runtime.h" |
15 | | |
16 | 0 | #define DEFAULT_PORT_HTTP "80" |
17 | 0 | #define DEFAULT_PORT_HTTPS "443" |
18 | 0 | #define DEFAULT_PORT_GIT "9418" |
19 | 0 | #define DEFAULT_PORT_SSH "22" |
20 | | |
21 | 0 | #define GIT_NET_URL_PARSER_INIT { 0 } |
22 | | |
23 | | typedef struct { |
24 | | unsigned int hierarchical : 1; |
25 | | |
26 | | const char *scheme; |
27 | | const char *user; |
28 | | const char *password; |
29 | | const char *host; |
30 | | const char *port; |
31 | | const char *path; |
32 | | const char *query; |
33 | | const char *fragment; |
34 | | |
35 | | size_t scheme_len; |
36 | | size_t user_len; |
37 | | size_t password_len; |
38 | | size_t host_len; |
39 | | size_t port_len; |
40 | | size_t path_len; |
41 | | size_t query_len; |
42 | | size_t fragment_len; |
43 | | } git_net_url_parser; |
44 | | |
45 | | bool git_net_hostname_matches_cert( |
46 | | const char *hostname, |
47 | | const char *pattern) |
48 | 0 | { |
49 | 0 | for (;;) { |
50 | 0 | char c = git__tolower(*pattern++); |
51 | |
|
52 | 0 | if (c == '\0') |
53 | 0 | return *hostname ? false : true; |
54 | | |
55 | 0 | if (c == '*') { |
56 | 0 | c = *pattern; |
57 | | |
58 | | /* '*' at the end matches everything left */ |
59 | 0 | if (c == '\0') |
60 | 0 | return true; |
61 | | |
62 | | /* |
63 | | * We've found a pattern, so move towards the |
64 | | * next matching char. The '.' is handled |
65 | | * specially because wildcards aren't allowed |
66 | | * to cross subdomains. |
67 | | */ |
68 | 0 | while(*hostname) { |
69 | 0 | char h = git__tolower(*hostname); |
70 | |
|
71 | 0 | if (h == c) |
72 | 0 | return git_net_hostname_matches_cert(hostname++, pattern); |
73 | 0 | else if (h == '.') |
74 | 0 | return git_net_hostname_matches_cert(hostname, pattern); |
75 | | |
76 | 0 | hostname++; |
77 | 0 | } |
78 | | |
79 | 0 | return false; |
80 | 0 | } |
81 | | |
82 | 0 | if (c != git__tolower(*hostname++)) |
83 | 0 | return false; |
84 | 0 | } |
85 | | |
86 | 0 | return false; |
87 | 0 | } |
88 | | |
89 | | #define is_valid_scheme_char(c) \ |
90 | 0 | (((c) >= 'a' && (c) <= 'z') || \ |
91 | 0 | ((c) >= 'A' && (c) <= 'Z') || \ |
92 | 0 | ((c) >= '0' && (c) <= '9') || \ |
93 | 0 | (c) == '+' || (c) == '-' || (c) == '.') |
94 | | |
95 | | bool git_net_str_is_url(const char *str) |
96 | 0 | { |
97 | 0 | const char *c; |
98 | |
|
99 | 0 | for (c = str; *c; c++) { |
100 | 0 | if (*c == ':' && *(c+1) == '/' && *(c+2) == '/') |
101 | 0 | return true; |
102 | | |
103 | 0 | if (!is_valid_scheme_char(*c)) |
104 | 0 | break; |
105 | 0 | } |
106 | | |
107 | 0 | return false; |
108 | 0 | } |
109 | | |
110 | | static const char *default_port_for_scheme(const char *scheme) |
111 | 0 | { |
112 | 0 | if (strcmp(scheme, "http") == 0) |
113 | 0 | return DEFAULT_PORT_HTTP; |
114 | 0 | else if (strcmp(scheme, "https") == 0) |
115 | 0 | return DEFAULT_PORT_HTTPS; |
116 | 0 | else if (strcmp(scheme, "git") == 0) |
117 | 0 | return DEFAULT_PORT_GIT; |
118 | 0 | else if (strcmp(scheme, "ssh") == 0 || |
119 | 0 | strcmp(scheme, "ssh+git") == 0 || |
120 | 0 | strcmp(scheme, "git+ssh") == 0) |
121 | 0 | return DEFAULT_PORT_SSH; |
122 | | |
123 | 0 | return NULL; |
124 | 0 | } |
125 | | |
126 | | static bool is_ssh_scheme(const char *scheme, size_t scheme_len) |
127 | 0 | { |
128 | 0 | if (!scheme_len) |
129 | 0 | return false; |
130 | | |
131 | 0 | return strncasecmp(scheme, "ssh", scheme_len) == 0 || |
132 | 0 | strncasecmp(scheme, "ssh+git", scheme_len) == 0 || |
133 | 0 | strncasecmp(scheme, "git+ssh", scheme_len) == 0; |
134 | 0 | } |
135 | | |
136 | | int git_net_url_dup(git_net_url *out, git_net_url *in) |
137 | 0 | { |
138 | 0 | if (in->scheme) { |
139 | 0 | out->scheme = git__strdup(in->scheme); |
140 | 0 | GIT_ERROR_CHECK_ALLOC(out->scheme); |
141 | 0 | } |
142 | | |
143 | 0 | if (in->host) { |
144 | 0 | out->host = git__strdup(in->host); |
145 | 0 | GIT_ERROR_CHECK_ALLOC(out->host); |
146 | 0 | } |
147 | | |
148 | 0 | if (in->port) { |
149 | 0 | out->port = git__strdup(in->port); |
150 | 0 | GIT_ERROR_CHECK_ALLOC(out->port); |
151 | 0 | } |
152 | | |
153 | 0 | if (in->path) { |
154 | 0 | out->path = git__strdup(in->path); |
155 | 0 | GIT_ERROR_CHECK_ALLOC(out->path); |
156 | 0 | } |
157 | | |
158 | 0 | if (in->query) { |
159 | 0 | out->query = git__strdup(in->query); |
160 | 0 | GIT_ERROR_CHECK_ALLOC(out->query); |
161 | 0 | } |
162 | | |
163 | 0 | if (in->username) { |
164 | 0 | out->username = git__strdup(in->username); |
165 | 0 | GIT_ERROR_CHECK_ALLOC(out->username); |
166 | 0 | } |
167 | | |
168 | 0 | if (in->password) { |
169 | 0 | out->password = git__strdup(in->password); |
170 | 0 | GIT_ERROR_CHECK_ALLOC(out->password); |
171 | 0 | } |
172 | | |
173 | 0 | return 0; |
174 | 0 | } |
175 | | |
176 | | static int url_invalid(const char *message) |
177 | 0 | { |
178 | 0 | git_error_set(GIT_ERROR_NET, "invalid url: %s", message); |
179 | 0 | return GIT_EINVALIDSPEC; |
180 | 0 | } |
181 | | |
182 | | static int url_parse_authority( |
183 | | git_net_url_parser *parser, |
184 | | const char *authority, |
185 | | size_t len) |
186 | 0 | { |
187 | 0 | const char *c, *hostport_end, *host_end = NULL, |
188 | 0 | *userpass_end, *user_end = NULL; |
189 | |
|
190 | 0 | enum { |
191 | 0 | HOSTPORT, HOST, IPV6, HOST_END, USERPASS, USER |
192 | 0 | } state = HOSTPORT; |
193 | |
|
194 | 0 | if (len == 0) |
195 | 0 | return 0; |
196 | | |
197 | | /* |
198 | | * walk the authority backwards so that we can parse google code's |
199 | | * ssh urls that are not rfc compliant and allow @ in the username |
200 | | */ |
201 | 0 | for (hostport_end = authority + len, c = hostport_end - 1; |
202 | 0 | c >= authority && !user_end; |
203 | 0 | c--) { |
204 | 0 | switch (state) { |
205 | 0 | case HOSTPORT: |
206 | 0 | if (*c == ':') { |
207 | 0 | parser->port = c + 1; |
208 | 0 | parser->port_len = hostport_end - parser->port; |
209 | 0 | host_end = c; |
210 | 0 | state = HOST; |
211 | 0 | break; |
212 | 0 | } |
213 | | |
214 | | /* |
215 | | * if we've only seen digits then we don't know |
216 | | * if we're parsing just a host or a host and port. |
217 | | * if we see a non-digit, then we're in a host, |
218 | | * otherwise, fall through to possibly match the |
219 | | * "@" (user/host separator). |
220 | | */ |
221 | | |
222 | 0 | if (*c < '0' || *c > '9') { |
223 | 0 | host_end = hostport_end; |
224 | 0 | state = HOST; |
225 | 0 | } |
226 | | |
227 | | /* fall through */ |
228 | |
|
229 | 0 | case HOST: |
230 | 0 | if (*c == ']' && host_end == c + 1) { |
231 | 0 | host_end = c; |
232 | 0 | state = IPV6; |
233 | 0 | } |
234 | | |
235 | 0 | else if (*c == '@') { |
236 | 0 | parser->host = c + 1; |
237 | 0 | parser->host_len = host_end ? |
238 | 0 | host_end - parser->host : |
239 | 0 | hostport_end - parser->host; |
240 | 0 | userpass_end = c; |
241 | 0 | state = USERPASS; |
242 | 0 | } |
243 | | |
244 | 0 | else if (*c == '[' || *c == ']' || *c == ':') { |
245 | 0 | return url_invalid("malformed hostname"); |
246 | 0 | } |
247 | | |
248 | 0 | break; |
249 | | |
250 | 0 | case IPV6: |
251 | 0 | if (*c == '[') { |
252 | 0 | parser->host = c + 1; |
253 | 0 | parser->host_len = host_end - parser->host; |
254 | 0 | state = HOST_END; |
255 | 0 | } |
256 | | |
257 | 0 | else if ((*c < '0' || *c > '9') && |
258 | 0 | (*c < 'a' || *c > 'f') && |
259 | 0 | (*c < 'A' || *c > 'F') && |
260 | 0 | (*c != ':')) { |
261 | 0 | return url_invalid("malformed hostname"); |
262 | 0 | } |
263 | | |
264 | 0 | break; |
265 | | |
266 | 0 | case HOST_END: |
267 | 0 | if (*c == '@') { |
268 | 0 | userpass_end = c; |
269 | 0 | state = USERPASS; |
270 | 0 | break; |
271 | 0 | } |
272 | | |
273 | 0 | return url_invalid("malformed hostname"); |
274 | | |
275 | 0 | case USERPASS: |
276 | 0 | if (*c == '@' && |
277 | 0 | !is_ssh_scheme(parser->scheme, parser->scheme_len)) |
278 | 0 | return url_invalid("malformed hostname"); |
279 | | |
280 | 0 | if (*c == ':') { |
281 | 0 | parser->password = c + 1; |
282 | 0 | parser->password_len = userpass_end - parser->password; |
283 | 0 | user_end = c; |
284 | 0 | state = USER; |
285 | 0 | break; |
286 | 0 | } |
287 | | |
288 | 0 | break; |
289 | | |
290 | 0 | default: |
291 | 0 | GIT_ASSERT(!"unhandled state"); |
292 | 0 | } |
293 | 0 | } |
294 | | |
295 | 0 | switch (state) { |
296 | 0 | case HOSTPORT: |
297 | 0 | parser->host = authority; |
298 | 0 | parser->host_len = (hostport_end - parser->host); |
299 | 0 | break; |
300 | 0 | case HOST: |
301 | 0 | parser->host = authority; |
302 | 0 | parser->host_len = (host_end - parser->host); |
303 | 0 | break; |
304 | 0 | case IPV6: |
305 | 0 | return url_invalid("malformed hostname"); |
306 | 0 | case HOST_END: |
307 | 0 | break; |
308 | 0 | case USERPASS: |
309 | 0 | parser->user = authority; |
310 | 0 | parser->user_len = (userpass_end - parser->user); |
311 | 0 | break; |
312 | 0 | case USER: |
313 | 0 | parser->user = authority; |
314 | 0 | parser->user_len = (user_end - parser->user); |
315 | 0 | break; |
316 | 0 | default: |
317 | 0 | GIT_ASSERT(!"unhandled state"); |
318 | 0 | } |
319 | | |
320 | 0 | return 0; |
321 | 0 | } |
322 | | |
323 | | static int url_parse_path( |
324 | | git_net_url_parser *parser, |
325 | | const char *path, |
326 | | size_t len) |
327 | 0 | { |
328 | 0 | const char *c, *end; |
329 | |
|
330 | 0 | enum { PATH, QUERY, FRAGMENT } state = PATH; |
331 | |
|
332 | 0 | parser->path = path; |
333 | 0 | end = path + len; |
334 | |
|
335 | 0 | for (c = path; c < end; c++) { |
336 | 0 | switch (state) { |
337 | 0 | case PATH: |
338 | 0 | switch (*c) { |
339 | 0 | case '?': |
340 | 0 | parser->path_len = (c - parser->path); |
341 | 0 | parser->query = c + 1; |
342 | 0 | state = QUERY; |
343 | 0 | break; |
344 | 0 | case '#': |
345 | 0 | parser->path_len = (c - parser->path); |
346 | 0 | parser->fragment = c + 1; |
347 | 0 | state = FRAGMENT; |
348 | 0 | break; |
349 | 0 | } |
350 | 0 | break; |
351 | | |
352 | 0 | case QUERY: |
353 | 0 | if (*c == '#') { |
354 | 0 | parser->query_len = (c - parser->query); |
355 | 0 | parser->fragment = c + 1; |
356 | 0 | state = FRAGMENT; |
357 | 0 | } |
358 | 0 | break; |
359 | | |
360 | 0 | case FRAGMENT: |
361 | 0 | break; |
362 | | |
363 | 0 | default: |
364 | 0 | GIT_ASSERT(!"unhandled state"); |
365 | 0 | } |
366 | 0 | } |
367 | | |
368 | 0 | switch (state) { |
369 | 0 | case PATH: |
370 | 0 | parser->path_len = (c - parser->path); |
371 | 0 | break; |
372 | 0 | case QUERY: |
373 | 0 | parser->query_len = (c - parser->query); |
374 | 0 | break; |
375 | 0 | case FRAGMENT: |
376 | 0 | parser->fragment_len = (c - parser->fragment); |
377 | 0 | break; |
378 | 0 | } |
379 | | |
380 | 0 | return 0; |
381 | 0 | } |
382 | | |
383 | | static int url_parse_finalize(git_net_url *url, git_net_url_parser *parser) |
384 | 0 | { |
385 | 0 | git_str scheme = GIT_STR_INIT, user = GIT_STR_INIT, |
386 | 0 | password = GIT_STR_INIT, host = GIT_STR_INIT, |
387 | 0 | port = GIT_STR_INIT, path = GIT_STR_INIT, |
388 | 0 | query = GIT_STR_INIT, fragment = GIT_STR_INIT; |
389 | 0 | const char *default_port; |
390 | 0 | int port_specified = 0; |
391 | 0 | int error = 0; |
392 | |
|
393 | 0 | if (parser->scheme_len) { |
394 | 0 | if ((error = git_str_put(&scheme, parser->scheme, parser->scheme_len)) < 0) |
395 | 0 | goto done; |
396 | | |
397 | 0 | git__strntolower(scheme.ptr, scheme.size); |
398 | 0 | } |
399 | | |
400 | 0 | if (parser->user_len && |
401 | 0 | (error = git_str_decode_percent(&user, parser->user, parser->user_len)) < 0) |
402 | 0 | goto done; |
403 | | |
404 | 0 | if (parser->password_len && |
405 | 0 | (error = git_str_decode_percent(&password, parser->password, parser->password_len)) < 0) |
406 | 0 | goto done; |
407 | | |
408 | 0 | if (parser->host_len && |
409 | 0 | (error = git_str_decode_percent(&host, parser->host, parser->host_len)) < 0) |
410 | 0 | goto done; |
411 | | |
412 | 0 | if (parser->port_len) { |
413 | 0 | port_specified = 1; |
414 | 0 | error = git_str_put(&port, parser->port, parser->port_len); |
415 | 0 | } else if (parser->scheme_len && |
416 | 0 | (default_port = default_port_for_scheme(scheme.ptr)) != NULL) { |
417 | 0 | error = git_str_puts(&port, default_port); |
418 | 0 | } |
419 | |
|
420 | 0 | if (error < 0) |
421 | 0 | goto done; |
422 | | |
423 | 0 | if (parser->path_len) |
424 | 0 | error = git_str_put(&path, parser->path, parser->path_len); |
425 | 0 | else if (parser->hierarchical) |
426 | 0 | error = git_str_puts(&path, "/"); |
427 | |
|
428 | 0 | if (error < 0) |
429 | 0 | goto done; |
430 | | |
431 | 0 | if (parser->query_len && |
432 | 0 | (error = git_str_decode_percent(&query, parser->query, parser->query_len)) < 0) |
433 | 0 | goto done; |
434 | | |
435 | 0 | if (parser->fragment_len && |
436 | 0 | (error = git_str_decode_percent(&fragment, parser->fragment, parser->fragment_len)) < 0) |
437 | 0 | goto done; |
438 | | |
439 | 0 | url->scheme = git_str_detach(&scheme); |
440 | 0 | url->host = git_str_detach(&host); |
441 | 0 | url->port = git_str_detach(&port); |
442 | 0 | url->path = git_str_detach(&path); |
443 | 0 | url->query = git_str_detach(&query); |
444 | 0 | url->fragment = git_str_detach(&fragment); |
445 | 0 | url->username = git_str_detach(&user); |
446 | 0 | url->password = git_str_detach(&password); |
447 | 0 | url->port_specified = port_specified; |
448 | |
|
449 | 0 | error = 0; |
450 | |
|
451 | 0 | done: |
452 | 0 | git_str_dispose(&scheme); |
453 | 0 | git_str_dispose(&user); |
454 | 0 | git_str_dispose(&password); |
455 | 0 | git_str_dispose(&host); |
456 | 0 | git_str_dispose(&port); |
457 | 0 | git_str_dispose(&path); |
458 | 0 | git_str_dispose(&query); |
459 | 0 | git_str_dispose(&fragment); |
460 | |
|
461 | 0 | return error; |
462 | 0 | } |
463 | | |
464 | | int git_net_url_parse(git_net_url *url, const char *given) |
465 | 0 | { |
466 | 0 | git_net_url_parser parser = GIT_NET_URL_PARSER_INIT; |
467 | 0 | const char *c, *authority, *path; |
468 | 0 | size_t authority_len = 0, path_len = 0; |
469 | 0 | int error = 0; |
470 | |
|
471 | 0 | enum { |
472 | 0 | SCHEME_START, SCHEME, |
473 | 0 | AUTHORITY_START, AUTHORITY, |
474 | 0 | PATH_START, PATH |
475 | 0 | } state = SCHEME_START; |
476 | |
|
477 | 0 | memset(url, 0, sizeof(git_net_url)); |
478 | |
|
479 | 0 | for (c = given; *c; c++) { |
480 | 0 | switch (state) { |
481 | 0 | case SCHEME_START: |
482 | 0 | parser.scheme = c; |
483 | 0 | state = SCHEME; |
484 | | |
485 | | /* fall through */ |
486 | |
|
487 | 0 | case SCHEME: |
488 | 0 | if (*c == ':') { |
489 | 0 | parser.scheme_len = (c - parser.scheme); |
490 | |
|
491 | 0 | if (parser.scheme_len && |
492 | 0 | *(c+1) == '/' && *(c+2) == '/') { |
493 | 0 | c += 2; |
494 | 0 | parser.hierarchical = 1; |
495 | 0 | state = AUTHORITY_START; |
496 | 0 | } else { |
497 | 0 | state = PATH_START; |
498 | 0 | } |
499 | 0 | } else if (!is_valid_scheme_char(*c)) { |
500 | | /* |
501 | | * an illegal scheme character means that we |
502 | | * were just given a relative path |
503 | | */ |
504 | 0 | path = given; |
505 | 0 | state = PATH; |
506 | 0 | break; |
507 | 0 | } |
508 | 0 | break; |
509 | | |
510 | 0 | case AUTHORITY_START: |
511 | 0 | authority = c; |
512 | 0 | state = AUTHORITY; |
513 | | |
514 | | /* fall through */ |
515 | 0 | case AUTHORITY: |
516 | 0 | if (*c != '/') |
517 | 0 | break; |
518 | | |
519 | 0 | authority_len = (c - authority); |
520 | | |
521 | | /* fall through */ |
522 | 0 | case PATH_START: |
523 | 0 | path = c; |
524 | 0 | state = PATH; |
525 | 0 | break; |
526 | | |
527 | 0 | case PATH: |
528 | 0 | break; |
529 | | |
530 | 0 | default: |
531 | 0 | GIT_ASSERT(!"unhandled state"); |
532 | 0 | } |
533 | 0 | } |
534 | | |
535 | 0 | switch (state) { |
536 | 0 | case SCHEME: |
537 | | /* |
538 | | * if we never saw a ':' then we were given a relative |
539 | | * path, not a bare scheme |
540 | | */ |
541 | 0 | path = given; |
542 | 0 | path_len = (c - path); |
543 | 0 | break; |
544 | 0 | case AUTHORITY_START: |
545 | 0 | break; |
546 | 0 | case AUTHORITY: |
547 | 0 | authority_len = (c - authority); |
548 | 0 | break; |
549 | 0 | case PATH_START: |
550 | 0 | break; |
551 | 0 | case PATH: |
552 | 0 | path_len = (c - path); |
553 | 0 | break; |
554 | 0 | default: |
555 | 0 | GIT_ASSERT(!"unhandled state"); |
556 | 0 | } |
557 | | |
558 | 0 | if (authority_len && |
559 | 0 | (error = url_parse_authority(&parser, authority, authority_len)) < 0) |
560 | 0 | goto done; |
561 | | |
562 | 0 | if (path_len && |
563 | 0 | (error = url_parse_path(&parser, path, path_len)) < 0) |
564 | 0 | goto done; |
565 | | |
566 | 0 | error = url_parse_finalize(url, &parser); |
567 | |
|
568 | 0 | done: |
569 | 0 | return error; |
570 | 0 | } |
571 | | |
572 | | int git_net_url_parse_http( |
573 | | git_net_url *url, |
574 | | const char *given) |
575 | 0 | { |
576 | 0 | git_net_url_parser parser = GIT_NET_URL_PARSER_INIT; |
577 | 0 | const char *c, *authority, *path = NULL; |
578 | 0 | size_t authority_len = 0, path_len = 0; |
579 | 0 | int error; |
580 | | |
581 | | /* Hopefully this is a proper URL with a scheme. */ |
582 | 0 | if (git_net_str_is_url(given)) |
583 | 0 | return git_net_url_parse(url, given); |
584 | | |
585 | 0 | memset(url, 0, sizeof(git_net_url)); |
586 | | |
587 | | /* Without a scheme, we are in the host (authority) section. */ |
588 | 0 | for (c = authority = given; *c; c++) { |
589 | 0 | if (!path && *c == '/') { |
590 | 0 | authority_len = (c - authority); |
591 | 0 | path = c; |
592 | 0 | } |
593 | 0 | } |
594 | |
|
595 | 0 | if (path) |
596 | 0 | path_len = (c - path); |
597 | 0 | else |
598 | 0 | authority_len = (c - authority); |
599 | |
|
600 | 0 | parser.scheme = "http"; |
601 | 0 | parser.scheme_len = 4; |
602 | 0 | parser.hierarchical = 1; |
603 | |
|
604 | 0 | if (authority_len && |
605 | 0 | (error = url_parse_authority(&parser, authority, authority_len)) < 0) |
606 | 0 | return error; |
607 | | |
608 | 0 | if (path_len && |
609 | 0 | (error = url_parse_path(&parser, path, path_len)) < 0) |
610 | 0 | return error; |
611 | | |
612 | 0 | return url_parse_finalize(url, &parser); |
613 | 0 | } |
614 | | |
615 | | static int scp_invalid(const char *message) |
616 | 0 | { |
617 | 0 | git_error_set(GIT_ERROR_NET, "invalid scp-style path: %s", message); |
618 | 0 | return GIT_EINVALIDSPEC; |
619 | 0 | } |
620 | | |
621 | | static bool is_ipv6(const char *str) |
622 | 0 | { |
623 | 0 | const char *c; |
624 | 0 | size_t colons = 0; |
625 | |
|
626 | 0 | if (*str++ != '[') |
627 | 0 | return false; |
628 | | |
629 | 0 | for (c = str; *c; c++) { |
630 | 0 | if (*c == ':') |
631 | 0 | colons++; |
632 | |
|
633 | 0 | if (*c == ']') |
634 | 0 | return (colons > 1); |
635 | | |
636 | 0 | if (*c != ':' && |
637 | 0 | (*c < '0' || *c > '9') && |
638 | 0 | (*c < 'a' || *c > 'f') && |
639 | 0 | (*c < 'A' || *c > 'F')) |
640 | 0 | return false; |
641 | 0 | } |
642 | | |
643 | 0 | return false; |
644 | 0 | } |
645 | | |
646 | | static bool has_at(const char *str) |
647 | 0 | { |
648 | 0 | const char *c; |
649 | |
|
650 | 0 | for (c = str; *c; c++) { |
651 | 0 | if (*c == '@') |
652 | 0 | return true; |
653 | | |
654 | 0 | if (*c == ':') |
655 | 0 | break; |
656 | 0 | } |
657 | | |
658 | 0 | return false; |
659 | 0 | } |
660 | | |
661 | | int git_net_url_parse_scp(git_net_url *url, const char *given) |
662 | 0 | { |
663 | 0 | const char *default_port = default_port_for_scheme("ssh"); |
664 | 0 | const char *c, *user, *host, *port = NULL, *path = NULL; |
665 | 0 | size_t user_len = 0, host_len = 0, port_len = 0; |
666 | 0 | unsigned short bracket = 0; |
667 | |
|
668 | 0 | enum { |
669 | 0 | NONE, |
670 | 0 | USER, |
671 | 0 | HOST_START, HOST, HOST_END, |
672 | 0 | IPV6, IPV6_END, |
673 | 0 | PORT_START, PORT, PORT_END, |
674 | 0 | PATH_START |
675 | 0 | } state = NONE; |
676 | |
|
677 | 0 | memset(url, 0, sizeof(git_net_url)); |
678 | |
|
679 | 0 | for (c = given; *c && !path; c++) { |
680 | 0 | switch (state) { |
681 | 0 | case NONE: |
682 | 0 | switch (*c) { |
683 | 0 | case '@': |
684 | 0 | return scp_invalid("unexpected '@'"); |
685 | 0 | case ':': |
686 | 0 | return scp_invalid("unexpected ':'"); |
687 | 0 | case '[': |
688 | 0 | if (is_ipv6(c)) { |
689 | 0 | state = IPV6; |
690 | 0 | host = c; |
691 | 0 | } else if (bracket++ > 1) { |
692 | 0 | return scp_invalid("unexpected '['"); |
693 | 0 | } |
694 | 0 | break; |
695 | 0 | default: |
696 | 0 | if (has_at(c)) { |
697 | 0 | state = USER; |
698 | 0 | user = c; |
699 | 0 | } else { |
700 | 0 | state = HOST; |
701 | 0 | host = c; |
702 | 0 | } |
703 | 0 | break; |
704 | 0 | } |
705 | 0 | break; |
706 | | |
707 | 0 | case USER: |
708 | 0 | if (*c == '@') { |
709 | 0 | user_len = (c - user); |
710 | 0 | state = HOST_START; |
711 | 0 | } |
712 | 0 | break; |
713 | | |
714 | 0 | case HOST_START: |
715 | 0 | state = (*c == '[') ? IPV6 : HOST; |
716 | 0 | host = c; |
717 | 0 | break; |
718 | | |
719 | 0 | case HOST: |
720 | 0 | if (*c == ':') { |
721 | 0 | host_len = (c - host); |
722 | 0 | state = bracket ? PORT_START : PATH_START; |
723 | 0 | } else if (*c == ']') { |
724 | 0 | if (bracket-- == 0) |
725 | 0 | return scp_invalid("unexpected ']'"); |
726 | | |
727 | 0 | host_len = (c - host); |
728 | 0 | state = HOST_END; |
729 | 0 | } |
730 | 0 | break; |
731 | | |
732 | 0 | case HOST_END: |
733 | 0 | if (*c != ':') |
734 | 0 | return scp_invalid("unexpected character after hostname"); |
735 | 0 | state = PATH_START; |
736 | 0 | break; |
737 | | |
738 | 0 | case IPV6: |
739 | 0 | if (*c == ']') |
740 | 0 | state = IPV6_END; |
741 | 0 | break; |
742 | | |
743 | 0 | case IPV6_END: |
744 | 0 | if (*c != ':') |
745 | 0 | return scp_invalid("unexpected character after ipv6 address"); |
746 | | |
747 | 0 | host_len = (c - host); |
748 | 0 | state = bracket ? PORT_START : PATH_START; |
749 | 0 | break; |
750 | | |
751 | 0 | case PORT_START: |
752 | 0 | port = c; |
753 | 0 | state = PORT; |
754 | 0 | break; |
755 | | |
756 | 0 | case PORT: |
757 | 0 | if (*c == ']') { |
758 | 0 | if (bracket-- == 0) |
759 | 0 | return scp_invalid("unexpected ']'"); |
760 | | |
761 | 0 | port_len = c - port; |
762 | 0 | state = PORT_END; |
763 | 0 | } |
764 | 0 | break; |
765 | | |
766 | 0 | case PORT_END: |
767 | 0 | if (*c != ':') |
768 | 0 | return scp_invalid("unexpected character after ipv6 address"); |
769 | | |
770 | 0 | state = PATH_START; |
771 | 0 | break; |
772 | | |
773 | 0 | case PATH_START: |
774 | 0 | path = c; |
775 | 0 | break; |
776 | | |
777 | 0 | default: |
778 | 0 | GIT_ASSERT(!"unhandled state"); |
779 | 0 | } |
780 | 0 | } |
781 | | |
782 | 0 | if (!path) |
783 | 0 | return scp_invalid("path is required"); |
784 | | |
785 | 0 | GIT_ERROR_CHECK_ALLOC(url->scheme = git__strdup("ssh")); |
786 | | |
787 | 0 | if (user_len) |
788 | 0 | GIT_ERROR_CHECK_ALLOC(url->username = git__strndup(user, user_len)); |
789 | | |
790 | 0 | GIT_ASSERT(host_len); |
791 | 0 | GIT_ERROR_CHECK_ALLOC(url->host = git__strndup(host, host_len)); |
792 | | |
793 | 0 | if (port_len) { |
794 | 0 | url->port_specified = 1; |
795 | 0 | GIT_ERROR_CHECK_ALLOC(url->port = git__strndup(port, port_len)); |
796 | 0 | } else { |
797 | 0 | GIT_ERROR_CHECK_ALLOC(url->port = git__strdup(default_port)); |
798 | 0 | } |
799 | | |
800 | 0 | GIT_ASSERT(path); |
801 | 0 | GIT_ERROR_CHECK_ALLOC(url->path = git__strdup(path)); |
802 | | |
803 | 0 | return 0; |
804 | 0 | } |
805 | | |
806 | | int git_net_url_parse_standard_or_scp(git_net_url *url, const char *given) |
807 | 0 | { |
808 | 0 | return git_net_str_is_url(given) ? |
809 | 0 | git_net_url_parse(url, given) : |
810 | 0 | git_net_url_parse_scp(url, given); |
811 | 0 | } |
812 | | |
813 | | int git_net_url_joinpath( |
814 | | git_net_url *out, |
815 | | git_net_url *one, |
816 | | const char *two) |
817 | 0 | { |
818 | 0 | git_str path = GIT_STR_INIT; |
819 | 0 | const char *query; |
820 | 0 | size_t one_len, two_len; |
821 | |
|
822 | 0 | git_net_url_dispose(out); |
823 | |
|
824 | 0 | if ((query = strchr(two, '?')) != NULL) { |
825 | 0 | two_len = query - two; |
826 | |
|
827 | 0 | if (*(++query) != '\0') { |
828 | 0 | out->query = git__strdup(query); |
829 | 0 | GIT_ERROR_CHECK_ALLOC(out->query); |
830 | 0 | } |
831 | 0 | } else { |
832 | 0 | two_len = strlen(two); |
833 | 0 | } |
834 | | |
835 | | /* Strip all trailing `/`s from the first path */ |
836 | 0 | one_len = one->path ? strlen(one->path) : 0; |
837 | 0 | while (one_len && one->path[one_len - 1] == '/') |
838 | 0 | one_len--; |
839 | | |
840 | | /* Strip all leading `/`s from the second path */ |
841 | 0 | while (*two == '/') { |
842 | 0 | two++; |
843 | 0 | two_len--; |
844 | 0 | } |
845 | |
|
846 | 0 | git_str_put(&path, one->path, one_len); |
847 | 0 | git_str_putc(&path, '/'); |
848 | 0 | git_str_put(&path, two, two_len); |
849 | |
|
850 | 0 | if (git_str_oom(&path)) |
851 | 0 | return -1; |
852 | | |
853 | 0 | out->path = git_str_detach(&path); |
854 | |
|
855 | 0 | if (one->scheme) { |
856 | 0 | out->scheme = git__strdup(one->scheme); |
857 | 0 | GIT_ERROR_CHECK_ALLOC(out->scheme); |
858 | 0 | } |
859 | | |
860 | 0 | if (one->host) { |
861 | 0 | out->host = git__strdup(one->host); |
862 | 0 | GIT_ERROR_CHECK_ALLOC(out->host); |
863 | 0 | } |
864 | | |
865 | 0 | if (one->port) { |
866 | 0 | out->port = git__strdup(one->port); |
867 | 0 | GIT_ERROR_CHECK_ALLOC(out->port); |
868 | 0 | } |
869 | | |
870 | 0 | if (one->username) { |
871 | 0 | out->username = git__strdup(one->username); |
872 | 0 | GIT_ERROR_CHECK_ALLOC(out->username); |
873 | 0 | } |
874 | | |
875 | 0 | if (one->password) { |
876 | 0 | out->password = git__strdup(one->password); |
877 | 0 | GIT_ERROR_CHECK_ALLOC(out->password); |
878 | 0 | } |
879 | | |
880 | 0 | return 0; |
881 | 0 | } |
882 | | |
883 | | /* |
884 | | * Some servers strip the query parameters from the Location header |
885 | | * when sending a redirect. Others leave it in place. |
886 | | * Check for both, starting with the stripped case first, |
887 | | * since it appears to be more common. |
888 | | */ |
889 | | static void remove_service_suffix( |
890 | | git_net_url *url, |
891 | | const char *service_suffix) |
892 | 0 | { |
893 | 0 | const char *service_query = strchr(service_suffix, '?'); |
894 | 0 | size_t full_suffix_len = strlen(service_suffix); |
895 | 0 | size_t suffix_len = service_query ? |
896 | 0 | (size_t)(service_query - service_suffix) : full_suffix_len; |
897 | 0 | size_t path_len = strlen(url->path); |
898 | 0 | ssize_t truncate = -1; |
899 | | |
900 | | /* |
901 | | * Check for a redirect without query parameters, |
902 | | * like "/newloc/info/refs"' |
903 | | */ |
904 | 0 | if (suffix_len && path_len >= suffix_len) { |
905 | 0 | size_t suffix_offset = path_len - suffix_len; |
906 | |
|
907 | 0 | if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && |
908 | 0 | (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { |
909 | 0 | truncate = suffix_offset; |
910 | 0 | } |
911 | 0 | } |
912 | | |
913 | | /* |
914 | | * If we haven't already found where to truncate to remove the |
915 | | * suffix, check for a redirect with query parameters, like |
916 | | * "/newloc/info/refs?service=git-upload-pack" |
917 | | */ |
918 | 0 | if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0) |
919 | 0 | truncate = path_len - full_suffix_len; |
920 | | |
921 | | /* Ensure we leave a minimum of '/' as the path */ |
922 | 0 | if (truncate == 0) |
923 | 0 | truncate++; |
924 | |
|
925 | 0 | if (truncate > 0) { |
926 | 0 | url->path[truncate] = '\0'; |
927 | |
|
928 | 0 | git__free(url->query); |
929 | 0 | url->query = NULL; |
930 | 0 | } |
931 | 0 | } |
932 | | |
933 | | int git_net_url_apply_redirect( |
934 | | git_net_url *url, |
935 | | const char *redirect_location, |
936 | | bool allow_offsite, |
937 | | const char *service_suffix) |
938 | 0 | { |
939 | 0 | git_net_url tmp = GIT_NET_URL_INIT; |
940 | 0 | int error = 0; |
941 | |
|
942 | 0 | GIT_ASSERT(url); |
943 | 0 | GIT_ASSERT(redirect_location); |
944 | | |
945 | 0 | if (redirect_location[0] == '/') { |
946 | 0 | git__free(url->path); |
947 | |
|
948 | 0 | if ((url->path = git__strdup(redirect_location)) == NULL) { |
949 | 0 | error = -1; |
950 | 0 | goto done; |
951 | 0 | } |
952 | 0 | } else { |
953 | 0 | git_net_url *original = url; |
954 | |
|
955 | 0 | if ((error = git_net_url_parse(&tmp, redirect_location)) < 0) |
956 | 0 | goto done; |
957 | | |
958 | | /* Validate that this is a legal redirection */ |
959 | | |
960 | 0 | if (original->scheme && |
961 | 0 | strcmp(original->scheme, tmp.scheme) != 0 && |
962 | 0 | strcmp(tmp.scheme, "https") != 0) { |
963 | 0 | git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", |
964 | 0 | original->scheme, tmp.scheme); |
965 | |
|
966 | 0 | error = -1; |
967 | 0 | goto done; |
968 | 0 | } |
969 | | |
970 | 0 | if (original->host && |
971 | 0 | !allow_offsite && |
972 | 0 | git__strcasecmp(original->host, tmp.host) != 0) { |
973 | 0 | git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", |
974 | 0 | original->host, tmp.host); |
975 | |
|
976 | 0 | error = -1; |
977 | 0 | goto done; |
978 | 0 | } |
979 | | |
980 | 0 | git_net_url_swap(url, &tmp); |
981 | 0 | } |
982 | | |
983 | | /* Remove the service suffix if it was given to us */ |
984 | 0 | if (service_suffix) |
985 | 0 | remove_service_suffix(url, service_suffix); |
986 | |
|
987 | 0 | done: |
988 | 0 | git_net_url_dispose(&tmp); |
989 | 0 | return error; |
990 | 0 | } |
991 | | |
992 | | bool git_net_url_valid(git_net_url *url) |
993 | 0 | { |
994 | 0 | return (url->host && url->port && url->path); |
995 | 0 | } |
996 | | |
997 | | bool git_net_url_is_default_port(git_net_url *url) |
998 | 0 | { |
999 | 0 | const char *default_port; |
1000 | |
|
1001 | 0 | if (url->scheme && (default_port = default_port_for_scheme(url->scheme)) != NULL) |
1002 | 0 | return (strcmp(url->port, default_port) == 0); |
1003 | 0 | else |
1004 | 0 | return false; |
1005 | 0 | } |
1006 | | |
1007 | | bool git_net_url_is_ipv6(git_net_url *url) |
1008 | 0 | { |
1009 | 0 | return (strchr(url->host, ':') != NULL); |
1010 | 0 | } |
1011 | | |
1012 | | void git_net_url_swap(git_net_url *a, git_net_url *b) |
1013 | 0 | { |
1014 | 0 | git_net_url tmp = GIT_NET_URL_INIT; |
1015 | |
|
1016 | 0 | memcpy(&tmp, a, sizeof(git_net_url)); |
1017 | 0 | memcpy(a, b, sizeof(git_net_url)); |
1018 | 0 | memcpy(b, &tmp, sizeof(git_net_url)); |
1019 | 0 | } |
1020 | | |
1021 | | int git_net_url_fmt(git_str *buf, git_net_url *url) |
1022 | 0 | { |
1023 | 0 | GIT_ASSERT_ARG(url); |
1024 | 0 | GIT_ASSERT_ARG(url->scheme); |
1025 | 0 | GIT_ASSERT_ARG(url->host); |
1026 | | |
1027 | 0 | git_str_puts(buf, url->scheme); |
1028 | 0 | git_str_puts(buf, "://"); |
1029 | |
|
1030 | 0 | if (url->username) { |
1031 | 0 | git_str_puts(buf, url->username); |
1032 | |
|
1033 | 0 | if (url->password) { |
1034 | 0 | git_str_puts(buf, ":"); |
1035 | 0 | git_str_puts(buf, url->password); |
1036 | 0 | } |
1037 | |
|
1038 | 0 | git_str_putc(buf, '@'); |
1039 | 0 | } |
1040 | |
|
1041 | 0 | git_str_puts(buf, url->host); |
1042 | |
|
1043 | 0 | if (url->port && !git_net_url_is_default_port(url)) { |
1044 | 0 | git_str_putc(buf, ':'); |
1045 | 0 | git_str_puts(buf, url->port); |
1046 | 0 | } |
1047 | |
|
1048 | 0 | git_str_puts(buf, url->path ? url->path : "/"); |
1049 | |
|
1050 | 0 | if (url->query) { |
1051 | 0 | git_str_putc(buf, '?'); |
1052 | 0 | git_str_puts(buf, url->query); |
1053 | 0 | } |
1054 | |
|
1055 | 0 | return git_str_oom(buf) ? -1 : 0; |
1056 | 0 | } |
1057 | | |
1058 | | int git_net_url_fmt_path(git_str *buf, git_net_url *url) |
1059 | 0 | { |
1060 | 0 | git_str_puts(buf, url->path ? url->path : "/"); |
1061 | |
|
1062 | 0 | if (url->query) { |
1063 | 0 | git_str_putc(buf, '?'); |
1064 | 0 | git_str_puts(buf, url->query); |
1065 | 0 | } |
1066 | |
|
1067 | 0 | return git_str_oom(buf) ? -1 : 0; |
1068 | 0 | } |
1069 | | |
1070 | | static bool matches_pattern( |
1071 | | git_net_url *url, |
1072 | | const char *pattern, |
1073 | | size_t pattern_len) |
1074 | 0 | { |
1075 | 0 | const char *domain, *port = NULL, *colon; |
1076 | 0 | size_t host_len, domain_len, port_len = 0, wildcard = 0; |
1077 | |
|
1078 | 0 | GIT_UNUSED(url); |
1079 | 0 | GIT_UNUSED(pattern); |
1080 | |
|
1081 | 0 | if (!pattern_len) |
1082 | 0 | return false; |
1083 | 0 | else if (pattern_len == 1 && pattern[0] == '*') |
1084 | 0 | return true; |
1085 | 0 | else if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.') |
1086 | 0 | wildcard = 2; |
1087 | 0 | else if (pattern[0] == '.') |
1088 | 0 | wildcard = 1; |
1089 | | |
1090 | 0 | domain = pattern + wildcard; |
1091 | 0 | domain_len = pattern_len - wildcard; |
1092 | |
|
1093 | 0 | if ((colon = memchr(domain, ':', domain_len)) != NULL) { |
1094 | 0 | domain_len = colon - domain; |
1095 | 0 | port = colon + 1; |
1096 | 0 | port_len = pattern_len - wildcard - domain_len - 1; |
1097 | 0 | } |
1098 | | |
1099 | | /* A pattern's port *must* match if it's specified */ |
1100 | 0 | if (port_len && git__strlcmp(url->port, port, port_len) != 0) |
1101 | 0 | return false; |
1102 | | |
1103 | | /* No wildcard? Host must match exactly. */ |
1104 | 0 | if (!wildcard) |
1105 | 0 | return !git__strlcmp(url->host, domain, domain_len); |
1106 | | |
1107 | | /* Wildcard: ensure there's (at least) a suffix match */ |
1108 | 0 | if ((host_len = strlen(url->host)) < domain_len || |
1109 | 0 | memcmp(url->host + (host_len - domain_len), domain, domain_len)) |
1110 | 0 | return false; |
1111 | | |
1112 | | /* The pattern is *.domain and the host is simply domain */ |
1113 | 0 | if (host_len == domain_len) |
1114 | 0 | return true; |
1115 | | |
1116 | | /* The pattern is *.domain and the host is foo.domain */ |
1117 | 0 | return (url->host[host_len - domain_len - 1] == '.'); |
1118 | 0 | } |
1119 | | |
1120 | | bool git_net_url_matches_pattern(git_net_url *url, const char *pattern) |
1121 | 0 | { |
1122 | 0 | return matches_pattern(url, pattern, strlen(pattern)); |
1123 | 0 | } |
1124 | | |
1125 | | bool git_net_url_matches_pattern_list( |
1126 | | git_net_url *url, |
1127 | | const char *pattern_list) |
1128 | 0 | { |
1129 | 0 | const char *pattern, *pattern_end, *sep; |
1130 | |
|
1131 | 0 | for (pattern = pattern_list; |
1132 | 0 | pattern && *pattern; |
1133 | 0 | pattern = sep ? sep + 1 : NULL) { |
1134 | 0 | sep = strchr(pattern, ','); |
1135 | 0 | pattern_end = sep ? sep : strchr(pattern, '\0'); |
1136 | |
|
1137 | 0 | if (matches_pattern(url, pattern, (pattern_end - pattern))) |
1138 | 0 | return true; |
1139 | 0 | } |
1140 | | |
1141 | 0 | return false; |
1142 | 0 | } |
1143 | | |
1144 | | void git_net_url_dispose(git_net_url *url) |
1145 | 0 | { |
1146 | 0 | if (url->username) |
1147 | 0 | git__memzero(url->username, strlen(url->username)); |
1148 | |
|
1149 | 0 | if (url->password) |
1150 | 0 | git__memzero(url->password, strlen(url->password)); |
1151 | |
|
1152 | 0 | git__free(url->scheme); url->scheme = NULL; |
1153 | 0 | git__free(url->host); url->host = NULL; |
1154 | 0 | git__free(url->port); url->port = NULL; |
1155 | 0 | git__free(url->path); url->path = NULL; |
1156 | 0 | git__free(url->query); url->query = NULL; |
1157 | 0 | git__free(url->fragment); url->fragment = NULL; |
1158 | 0 | git__free(url->username); url->username = NULL; |
1159 | | git__free(url->password); url->password = NULL; |
1160 | 0 | } |