Coverage Report

Created: 2025-10-10 06:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}