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_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 GIT_ASSERT(url);
285 GIT_ASSERT(redirect_location);
286
287 if (redirect_location[0] == '/') {
288 git__free(url->path);
289
290 if ((url->path = git__strdup(redirect_location)) == NULL) {
291 error = -1;
292 goto done;
293 }
294 } else {
295 git_net_url *original = url;
296
297 if ((error = git_net_url_parse(&tmp, redirect_location)) < 0)
298 goto done;
299
300 /* Validate that this is a legal redirection */
301
302 if (original->scheme &&
303 strcmp(original->scheme, tmp.scheme) != 0 &&
304 strcmp(tmp.scheme, "https") != 0) {
305 git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
306 original->scheme, tmp.scheme);
307
308 error = -1;
309 goto done;
310 }
311
312 if (original->host &&
313 git__strcasecmp(original->host, tmp.host) != 0) {
314 git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'",
315 original->host, tmp.host);
316
317 error = -1;
318 goto done;
319 }
320
321 git_net_url_swap(url, &tmp);
322 }
323
324 /* Remove the service suffix if it was given to us */
325 if (service_suffix)
326 remove_service_suffix(url, service_suffix);
327
328 done:
329 git_net_url_dispose(&tmp);
330 return error;
331 }
332
git_net_url_valid(git_net_url * url)333 bool git_net_url_valid(git_net_url *url)
334 {
335 return (url->host && url->port && url->path);
336 }
337
git_net_url_is_default_port(git_net_url * url)338 bool git_net_url_is_default_port(git_net_url *url)
339 {
340 const char *default_port;
341
342 if ((default_port = default_port_for_scheme(url->scheme)) != NULL)
343 return (strcmp(url->port, default_port) == 0);
344 else
345 return false;
346 }
347
git_net_url_is_ipv6(git_net_url * url)348 bool git_net_url_is_ipv6(git_net_url *url)
349 {
350 return (strchr(url->host, ':') != NULL);
351 }
352
git_net_url_swap(git_net_url * a,git_net_url * b)353 void git_net_url_swap(git_net_url *a, git_net_url *b)
354 {
355 git_net_url tmp = GIT_NET_URL_INIT;
356
357 memcpy(&tmp, a, sizeof(git_net_url));
358 memcpy(a, b, sizeof(git_net_url));
359 memcpy(b, &tmp, sizeof(git_net_url));
360 }
361
git_net_url_fmt(git_buf * buf,git_net_url * url)362 int git_net_url_fmt(git_buf *buf, git_net_url *url)
363 {
364 git_buf_puts(buf, url->scheme);
365 git_buf_puts(buf, "://");
366
367 if (url->username) {
368 git_buf_puts(buf, url->username);
369
370 if (url->password) {
371 git_buf_puts(buf, ":");
372 git_buf_puts(buf, url->password);
373 }
374
375 git_buf_putc(buf, '@');
376 }
377
378 git_buf_puts(buf, url->host);
379
380 if (url->port && !git_net_url_is_default_port(url)) {
381 git_buf_putc(buf, ':');
382 git_buf_puts(buf, url->port);
383 }
384
385 git_buf_puts(buf, url->path ? url->path : "/");
386
387 if (url->query) {
388 git_buf_putc(buf, '?');
389 git_buf_puts(buf, url->query);
390 }
391
392 return git_buf_oom(buf) ? -1 : 0;
393 }
394
git_net_url_fmt_path(git_buf * buf,git_net_url * url)395 int git_net_url_fmt_path(git_buf *buf, git_net_url *url)
396 {
397 git_buf_puts(buf, url->path ? url->path : "/");
398
399 if (url->query) {
400 git_buf_putc(buf, '?');
401 git_buf_puts(buf, url->query);
402 }
403
404 return git_buf_oom(buf) ? -1 : 0;
405 }
406
git_net_url_dispose(git_net_url * url)407 void git_net_url_dispose(git_net_url *url)
408 {
409 if (url->username)
410 git__memzero(url->username, strlen(url->username));
411
412 if (url->password)
413 git__memzero(url->password, strlen(url->password));
414
415 git__free(url->scheme); url->scheme = NULL;
416 git__free(url->host); url->host = NULL;
417 git__free(url->port); url->port = NULL;
418 git__free(url->path); url->path = NULL;
419 git__free(url->query); url->query = NULL;
420 git__free(url->username); url->username = NULL;
421 git__free(url->password); url->password = NULL;
422 }
423