| /* |
| * Copyright (C) the libgit2 contributors. All rights reserved. |
| * |
| * This file is part of libgit2, distributed under the GNU GPL v2 with |
| * a Linking Exception. For full terms see the included COPYING file. |
| */ |
| |
| #include "net.h" |
| #include "netops.h" |
| |
| #include <ctype.h> |
| #include "git2/errors.h" |
| |
| #include "posix.h" |
| #include "buffer.h" |
| #include "http_parser.h" |
| #include "global.h" |
| |
| #define DEFAULT_PORT_HTTP "80" |
| #define DEFAULT_PORT_HTTPS "443" |
| #define DEFAULT_PORT_GIT "9418" |
| #define DEFAULT_PORT_SSH "22" |
| |
| static const char *default_port_for_scheme(const char *scheme) |
| { |
| if (strcmp(scheme, "http") == 0) |
| return DEFAULT_PORT_HTTP; |
| else if (strcmp(scheme, "https") == 0) |
| return DEFAULT_PORT_HTTPS; |
| else if (strcmp(scheme, "git") == 0) |
| return DEFAULT_PORT_GIT; |
| else if (strcmp(scheme, "ssh") == 0) |
| return DEFAULT_PORT_SSH; |
| |
| return NULL; |
| } |
| |
| int git_net_url_parse(git_net_url *url, const char *given) |
| { |
| struct http_parser_url u = {0}; |
| bool has_scheme, has_host, has_port, has_path, has_query, has_userinfo; |
| git_buf scheme = GIT_BUF_INIT, |
| host = GIT_BUF_INIT, |
| port = GIT_BUF_INIT, |
| path = GIT_BUF_INIT, |
| username = GIT_BUF_INIT, |
| password = GIT_BUF_INIT, |
| query = GIT_BUF_INIT; |
| int error = GIT_EINVALIDSPEC; |
| |
| if (http_parser_parse_url(given, strlen(given), false, &u)) { |
| git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given); |
| goto done; |
| } |
| |
| has_scheme = !!(u.field_set & (1 << UF_SCHEMA)); |
| has_host = !!(u.field_set & (1 << UF_HOST)); |
| has_port = !!(u.field_set & (1 << UF_PORT)); |
| has_path = !!(u.field_set & (1 << UF_PATH)); |
| has_query = !!(u.field_set & (1 << UF_QUERY)); |
| has_userinfo = !!(u.field_set & (1 << UF_USERINFO)); |
| |
| if (has_scheme) { |
| const char *url_scheme = given + u.field_data[UF_SCHEMA].off; |
| size_t url_scheme_len = u.field_data[UF_SCHEMA].len; |
| git_buf_put(&scheme, url_scheme, url_scheme_len); |
| git__strntolower(scheme.ptr, scheme.size); |
| } else { |
| git_error_set(GIT_ERROR_NET, "malformed URL '%s'", given); |
| goto done; |
| } |
| |
| if (has_host) { |
| const char *url_host = given + u.field_data[UF_HOST].off; |
| size_t url_host_len = u.field_data[UF_HOST].len; |
| git_buf_decode_percent(&host, url_host, url_host_len); |
| } |
| |
| if (has_port) { |
| const char *url_port = given + u.field_data[UF_PORT].off; |
| size_t url_port_len = u.field_data[UF_PORT].len; |
| git_buf_put(&port, url_port, url_port_len); |
| } else { |
| const char *default_port = default_port_for_scheme(scheme.ptr); |
| |
| if (default_port == NULL) { |
| git_error_set(GIT_ERROR_NET, "unknown scheme for URL '%s'", given); |
| goto done; |
| } |
| |
| git_buf_puts(&port, default_port); |
| } |
| |
| if (has_path) { |
| const char *url_path = given + u.field_data[UF_PATH].off; |
| size_t url_path_len = u.field_data[UF_PATH].len; |
| git_buf_put(&path, url_path, url_path_len); |
| } else { |
| git_buf_puts(&path, "/"); |
| } |
| |
| if (has_query) { |
| const char *url_query = given + u.field_data[UF_QUERY].off; |
| size_t url_query_len = u.field_data[UF_QUERY].len; |
| git_buf_decode_percent(&query, url_query, url_query_len); |
| } |
| |
| if (has_userinfo) { |
| const char *url_userinfo = given + u.field_data[UF_USERINFO].off; |
| size_t url_userinfo_len = u.field_data[UF_USERINFO].len; |
| const char *colon = memchr(url_userinfo, ':', url_userinfo_len); |
| |
| if (colon) { |
| const char *url_username = url_userinfo; |
| size_t url_username_len = colon - url_userinfo; |
| const char *url_password = colon + 1; |
| size_t url_password_len = url_userinfo_len - (url_username_len + 1); |
| |
| git_buf_decode_percent(&username, url_username, url_username_len); |
| git_buf_decode_percent(&password, url_password, url_password_len); |
| } else { |
| git_buf_decode_percent(&username, url_userinfo, url_userinfo_len); |
| } |
| } |
| |
| if (git_buf_oom(&scheme) || |
| git_buf_oom(&host) || |
| git_buf_oom(&port) || |
| git_buf_oom(&path) || |
| git_buf_oom(&query) || |
| git_buf_oom(&username) || |
| git_buf_oom(&password)) |
| return -1; |
| |
| url->scheme = git_buf_detach(&scheme); |
| url->host = git_buf_detach(&host); |
| url->port = git_buf_detach(&port); |
| url->path = git_buf_detach(&path); |
| url->query = git_buf_detach(&query); |
| url->username = git_buf_detach(&username); |
| url->password = git_buf_detach(&password); |
| |
| error = 0; |
| |
| done: |
| git_buf_dispose(&scheme); |
| git_buf_dispose(&host); |
| git_buf_dispose(&port); |
| git_buf_dispose(&path); |
| git_buf_dispose(&query); |
| git_buf_dispose(&username); |
| git_buf_dispose(&password); |
| return error; |
| } |
| |
| int git_net_url_joinpath( |
| git_net_url *out, |
| git_net_url *one, |
| const char *two) |
| { |
| git_buf path = GIT_BUF_INIT; |
| const char *query; |
| size_t one_len, two_len; |
| |
| git_net_url_dispose(out); |
| |
| if ((query = strchr(two, '?')) != NULL) { |
| two_len = query - two; |
| |
| if (*(++query) != '\0') { |
| out->query = git__strdup(query); |
| GIT_ERROR_CHECK_ALLOC(out->query); |
| } |
| } else { |
| two_len = strlen(two); |
| } |
| |
| /* Strip all trailing `/`s from the first path */ |
| one_len = one->path ? strlen(one->path) : 0; |
| while (one_len && one->path[one_len - 1] == '/') |
| one_len--; |
| |
| /* Strip all leading `/`s from the second path */ |
| while (*two == '/') { |
| two++; |
| two_len--; |
| } |
| |
| git_buf_put(&path, one->path, one_len); |
| git_buf_putc(&path, '/'); |
| git_buf_put(&path, two, two_len); |
| |
| if (git_buf_oom(&path)) |
| return -1; |
| |
| out->path = git_buf_detach(&path); |
| |
| if (one->scheme) { |
| out->scheme = git__strdup(one->scheme); |
| GIT_ERROR_CHECK_ALLOC(out->scheme); |
| } |
| |
| if (one->host) { |
| out->host = git__strdup(one->host); |
| GIT_ERROR_CHECK_ALLOC(out->host); |
| } |
| |
| if (one->port) { |
| out->port = git__strdup(one->port); |
| GIT_ERROR_CHECK_ALLOC(out->port); |
| } |
| |
| if (one->username) { |
| out->username = git__strdup(one->username); |
| GIT_ERROR_CHECK_ALLOC(out->username); |
| } |
| |
| if (one->password) { |
| out->password = git__strdup(one->password); |
| GIT_ERROR_CHECK_ALLOC(out->password); |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Some servers strip the query parameters from the Location header |
| * when sending a redirect. Others leave it in place. |
| * Check for both, starting with the stripped case first, |
| * since it appears to be more common. |
| */ |
| static void remove_service_suffix( |
| git_net_url *url, |
| const char *service_suffix) |
| { |
| const char *service_query = strchr(service_suffix, '?'); |
| size_t full_suffix_len = strlen(service_suffix); |
| size_t suffix_len = service_query ? |
| (size_t)(service_query - service_suffix) : full_suffix_len; |
| size_t path_len = strlen(url->path); |
| ssize_t truncate = -1; |
| |
| /* |
| * Check for a redirect without query parameters, |
| * like "/newloc/info/refs"' |
| */ |
| if (suffix_len && path_len >= suffix_len) { |
| size_t suffix_offset = path_len - suffix_len; |
| |
| if (git__strncmp(url->path + suffix_offset, service_suffix, suffix_len) == 0 && |
| (!service_query || git__strcmp(url->query, service_query + 1) == 0)) { |
| truncate = suffix_offset; |
| } |
| } |
| |
| /* |
| * If we haven't already found where to truncate to remove the |
| * suffix, check for a redirect with query parameters, like |
| * "/newloc/info/refs?service=git-upload-pack" |
| */ |
| if (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0) |
| truncate = path_len - full_suffix_len; |
| |
| /* Ensure we leave a minimum of '/' as the path */ |
| if (truncate == 0) |
| truncate++; |
| |
| if (truncate > 0) { |
| url->path[truncate] = '\0'; |
| |
| git__free(url->query); |
| url->query = NULL; |
| } |
| } |
| |
| int git_net_url_apply_redirect( |
| git_net_url *url, |
| const char *redirect_location, |
| const char *service_suffix) |
| { |
| git_net_url tmp = GIT_NET_URL_INIT; |
| int error = 0; |
| |
| assert(url && redirect_location); |
| |
| if (redirect_location[0] == '/') { |
| git__free(url->path); |
| |
| if ((url->path = git__strdup(redirect_location)) == NULL) { |
| error = -1; |
| goto done; |
| } |
| } else { |
| git_net_url *original = url; |
| |
| if ((error = git_net_url_parse(&tmp, redirect_location)) < 0) |
| goto done; |
| |
| /* Validate that this is a legal redirection */ |
| |
| if (original->scheme && |
| strcmp(original->scheme, tmp.scheme) != 0 && |
| strcmp(tmp.scheme, "https") != 0) { |
| git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", |
| original->scheme, tmp.scheme); |
| |
| error = -1; |
| goto done; |
| } |
| |
| if (original->host && |
| git__strcasecmp(original->host, tmp.host) != 0) { |
| git_error_set(GIT_ERROR_NET, "cannot redirect from '%s' to '%s'", |
| original->host, tmp.host); |
| |
| error = -1; |
| goto done; |
| } |
| |
| git_net_url_swap(url, &tmp); |
| } |
| |
| /* Remove the service suffix if it was given to us */ |
| if (service_suffix) |
| remove_service_suffix(url, service_suffix); |
| |
| done: |
| git_net_url_dispose(&tmp); |
| return error; |
| } |
| |
| bool git_net_url_valid(git_net_url *url) |
| { |
| return (url->host && url->port && url->path); |
| } |
| |
| int git_net_url_is_default_port(git_net_url *url) |
| { |
| return (strcmp(url->port, default_port_for_scheme(url->scheme)) == 0); |
| } |
| |
| void git_net_url_swap(git_net_url *a, git_net_url *b) |
| { |
| git_net_url tmp = GIT_NET_URL_INIT; |
| |
| memcpy(&tmp, a, sizeof(git_net_url)); |
| memcpy(a, b, sizeof(git_net_url)); |
| memcpy(b, &tmp, sizeof(git_net_url)); |
| } |
| |
| int git_net_url_fmt(git_buf *buf, git_net_url *url) |
| { |
| git_buf_puts(buf, url->scheme); |
| git_buf_puts(buf, "://"); |
| |
| if (url->username) { |
| git_buf_puts(buf, url->username); |
| |
| if (url->password) { |
| git_buf_puts(buf, ":"); |
| git_buf_puts(buf, url->password); |
| } |
| |
| git_buf_putc(buf, '@'); |
| } |
| |
| git_buf_puts(buf, url->host); |
| |
| if (url->port && !git_net_url_is_default_port(url)) { |
| git_buf_putc(buf, ':'); |
| git_buf_puts(buf, url->port); |
| } |
| |
| git_buf_puts(buf, url->path ? url->path : "/"); |
| |
| if (url->query) { |
| git_buf_putc(buf, '?'); |
| git_buf_puts(buf, url->query); |
| } |
| |
| return git_buf_oom(buf) ? -1 : 0; |
| } |
| |
| int git_net_url_fmt_path(git_buf *buf, git_net_url *url) |
| { |
| git_buf_puts(buf, url->path ? url->path : "/"); |
| |
| if (url->query) { |
| git_buf_putc(buf, '?'); |
| git_buf_puts(buf, url->query); |
| } |
| |
| return git_buf_oom(buf) ? -1 : 0; |
| } |
| |
| void git_net_url_dispose(git_net_url *url) |
| { |
| if (url->username) |
| git__memzero(url->username, strlen(url->username)); |
| |
| if (url->password) |
| git__memzero(url->password, strlen(url->password)); |
| |
| git__free(url->scheme); url->scheme = NULL; |
| git__free(url->host); url->host = NULL; |
| git__free(url->port); url->port = NULL; |
| git__free(url->path); url->path = NULL; |
| git__free(url->query); url->query = NULL; |
| git__free(url->username); url->username = NULL; |
| git__free(url->password); url->password = NULL; |
| } |