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