#include "net.h"
#include <ctype.h>
#include "posix.h"
#include "str.h"
#include "runtime.h"
#define DEFAULT_PORT_HTTP "80"
#define DEFAULT_PORT_HTTPS "443"
#define DEFAULT_PORT_GIT "9418"
#define DEFAULT_PORT_SSH "22"
#define GIT_NET_URL_PARSER_INIT { 0 }
typedef struct {
unsigned int hierarchical : 1;
const char *scheme;
const char *user;
const char *password;
const char *host;
const char *port;
const char *path;
const char *query;
const char *fragment;
size_t scheme_len;
size_t user_len;
size_t password_len;
size_t host_len;
size_t port_len;
size_t path_len;
size_t query_len;
size_t fragment_len;
} git_net_url_parser;
bool git_net_hostname_matches_cert(
const char *hostname,
const char *pattern)
{
for (;;) {
char c = git__tolower(*pattern++);
if (c == '\0')
return *hostname ? false : true;
if (c == '*') {
c = *pattern;
if (c == '\0')
return true;
while(*hostname) {
char h = git__tolower(*hostname);
if (h == c)
return git_net_hostname_matches_cert(hostname++, pattern);
else if (h == '.')
return git_net_hostname_matches_cert(hostname, pattern);
hostname++;
}
return false;
}
if (c != git__tolower(*hostname++))
return false;
}
return false;
}
#define is_valid_scheme_char(c) \
(((c) >= 'a' && (c) <= 'z') || \
((c) >= 'A' && (c) <= 'Z') || \
((c) >= '0' && (c) <= '9') || \
(c) == '+' || (c) == '-' || (c) == '.')
bool git_net_str_is_url(const char *str)
{
const char *c;
for (c = str; *c; c++) {
if (*c == ':' && *(c+1) == '/' && *(c+2) == '/')
return true;
if (!is_valid_scheme_char(*c))
break;
}
return false;
}
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 ||
strcmp(scheme, "ssh+git") == 0 ||
strcmp(scheme, "git+ssh") == 0)
return DEFAULT_PORT_SSH;
return NULL;
}
static bool is_ssh_scheme(const char *scheme, size_t scheme_len)
{
if (!scheme_len)
return false;
return strncasecmp(scheme, "ssh", scheme_len) == 0 ||
strncasecmp(scheme, "ssh+git", scheme_len) == 0 ||
strncasecmp(scheme, "git+ssh", scheme_len) == 0;
}
int git_net_url_dup(git_net_url *out, git_net_url *in)
{
if (in->scheme) {
out->scheme = git__strdup(in->scheme);
GIT_ERROR_CHECK_ALLOC(out->scheme);
}
if (in->host) {
out->host = git__strdup(in->host);
GIT_ERROR_CHECK_ALLOC(out->host);
}
if (in->port) {
out->port = git__strdup(in->port);
GIT_ERROR_CHECK_ALLOC(out->port);
}
if (in->path) {
out->path = git__strdup(in->path);
GIT_ERROR_CHECK_ALLOC(out->path);
}
if (in->query) {
out->query = git__strdup(in->query);
GIT_ERROR_CHECK_ALLOC(out->query);
}
if (in->username) {
out->username = git__strdup(in->username);
GIT_ERROR_CHECK_ALLOC(out->username);
}
if (in->password) {
out->password = git__strdup(in->password);
GIT_ERROR_CHECK_ALLOC(out->password);
}
return 0;
}
static int url_invalid(const char *message)
{
git_error_set(GIT_ERROR_NET, "invalid url: %s", message);
return GIT_EINVALIDSPEC;
}
static int url_parse_authority(
git_net_url_parser *parser,
const char *authority,
size_t len)
{
const char *c, *hostport_end, *host_end = NULL,
*userpass_end, *user_end = NULL;
enum {
HOSTPORT, HOST, IPV6, HOST_END, USERPASS, USER
} state = HOSTPORT;
if (len == 0)
return 0;
for (hostport_end = authority + len, c = hostport_end - 1;
c >= authority && !user_end;
c--) {
switch (state) {
case HOSTPORT:
if (*c == ':') {
parser->port = c + 1;
parser->port_len = hostport_end - parser->port;
host_end = c;
state = HOST;
break;
}
if (*c < '0' || *c > '9') {
host_end = hostport_end;
state = HOST;
}
case HOST:
if (*c == ']' && host_end == c + 1) {
host_end = c;
state = IPV6;
}
else if (*c == '@') {
parser->host = c + 1;
parser->host_len = host_end ?
host_end - parser->host :
hostport_end - parser->host;
userpass_end = c;
state = USERPASS;
}
else if (*c == '[' || *c == ']' || *c == ':') {
return url_invalid("malformed hostname");
}
break;
case IPV6:
if (*c == '[') {
parser->host = c + 1;
parser->host_len = host_end - parser->host;
state = HOST_END;
}
else if ((*c < '0' || *c > '9') &&
(*c < 'a' || *c > 'f') &&
(*c < 'A' || *c > 'F') &&
(*c != ':')) {
return url_invalid("malformed hostname");
}
break;
case HOST_END:
if (*c == '@') {
userpass_end = c;
state = USERPASS;
break;
}
return url_invalid("malformed hostname");
case USERPASS:
if (*c == '@' &&
!is_ssh_scheme(parser->scheme, parser->scheme_len))
return url_invalid("malformed hostname");
if (*c == ':') {
parser->password = c + 1;
parser->password_len = userpass_end - parser->password;
user_end = c;
state = USER;
break;
}
break;
default:
GIT_ASSERT(!"unhandled state");
}
}
switch (state) {
case HOSTPORT:
parser->host = authority;
parser->host_len = (hostport_end - parser->host);
break;
case HOST:
parser->host = authority;
parser->host_len = (host_end - parser->host);
break;
case IPV6:
return url_invalid("malformed hostname");
case HOST_END:
break;
case USERPASS:
parser->user = authority;
parser->user_len = (userpass_end - parser->user);
break;
case USER:
parser->user = authority;
parser->user_len = (user_end - parser->user);
break;
default:
GIT_ASSERT(!"unhandled state");
}
return 0;
}
static int url_parse_path(
git_net_url_parser *parser,
const char *path,
size_t len)
{
const char *c, *end;
enum { PATH, QUERY, FRAGMENT } state = PATH;
parser->path = path;
end = path + len;
for (c = path; c < end; c++) {
switch (state) {
case PATH:
switch (*c) {
case '?':
parser->path_len = (c - parser->path);
parser->query = c + 1;
state = QUERY;
break;
case '#':
parser->path_len = (c - parser->path);
parser->fragment = c + 1;
state = FRAGMENT;
break;
}
break;
case QUERY:
if (*c == '#') {
parser->query_len = (c - parser->query);
parser->fragment = c + 1;
state = FRAGMENT;
}
break;
case FRAGMENT:
break;
default:
GIT_ASSERT(!"unhandled state");
}
}
switch (state) {
case PATH:
parser->path_len = (c - parser->path);
break;
case QUERY:
parser->query_len = (c - parser->query);
break;
case FRAGMENT:
parser->fragment_len = (c - parser->fragment);
break;
}
return 0;
}
static int url_parse_finalize(git_net_url *url, git_net_url_parser *parser)
{
git_str scheme = GIT_STR_INIT, user = GIT_STR_INIT,
password = GIT_STR_INIT, host = GIT_STR_INIT,
port = GIT_STR_INIT, path = GIT_STR_INIT,
query = GIT_STR_INIT, fragment = GIT_STR_INIT;
const char *default_port;
int port_specified = 0;
int error = 0;
if (parser->scheme_len) {
if ((error = git_str_put(&scheme, parser->scheme, parser->scheme_len)) < 0)
goto done;
git__strntolower(scheme.ptr, scheme.size);
}
if (parser->user_len &&
(error = git_str_decode_percent(&user, parser->user, parser->user_len)) < 0)
goto done;
if (parser->password_len &&
(error = git_str_decode_percent(&password, parser->password, parser->password_len)) < 0)
goto done;
if (parser->host_len &&
(error = git_str_decode_percent(&host, parser->host, parser->host_len)) < 0)
goto done;
if (parser->port_len) {
port_specified = 1;
error = git_str_put(&port, parser->port, parser->port_len);
} else if (parser->scheme_len &&
(default_port = default_port_for_scheme(scheme.ptr)) != NULL) {
error = git_str_puts(&port, default_port);
}
if (error < 0)
goto done;
if (parser->path_len)
error = git_str_put(&path, parser->path, parser->path_len);
else if (parser->hierarchical)
error = git_str_puts(&path, "/");
if (error < 0)
goto done;
if (parser->query_len &&
(error = git_str_decode_percent(&query, parser->query, parser->query_len)) < 0)
goto done;
if (parser->fragment_len &&
(error = git_str_decode_percent(&fragment, parser->fragment, parser->fragment_len)) < 0)
goto done;
url->scheme = git_str_detach(&scheme);
url->host = git_str_detach(&host);
url->port = git_str_detach(&port);
url->path = git_str_detach(&path);
url->query = git_str_detach(&query);
url->fragment = git_str_detach(&fragment);
url->username = git_str_detach(&user);
url->password = git_str_detach(&password);
url->port_specified = port_specified;
error = 0;
done:
git_str_dispose(&scheme);
git_str_dispose(&user);
git_str_dispose(&password);
git_str_dispose(&host);
git_str_dispose(&port);
git_str_dispose(&path);
git_str_dispose(&query);
git_str_dispose(&fragment);
return error;
}
int git_net_url_parse(git_net_url *url, const char *given)
{
git_net_url_parser parser = GIT_NET_URL_PARSER_INIT;
const char *c, *authority, *path;
size_t authority_len = 0, path_len = 0;
int error = 0;
enum {
SCHEME_START, SCHEME,
AUTHORITY_START, AUTHORITY,
PATH_START, PATH
} state = SCHEME_START;
memset(url, 0, sizeof(git_net_url));
for (c = given; *c; c++) {
switch (state) {
case SCHEME_START:
parser.scheme = c;
state = SCHEME;
case SCHEME:
if (*c == ':') {
parser.scheme_len = (c - parser.scheme);
if (parser.scheme_len &&
*(c+1) == '/' && *(c+2) == '/') {
c += 2;
parser.hierarchical = 1;
state = AUTHORITY_START;
} else {
state = PATH_START;
}
} else if (!is_valid_scheme_char(*c)) {
path = given;
state = PATH;
break;
}
break;
case AUTHORITY_START:
authority = c;
state = AUTHORITY;
case AUTHORITY:
if (*c != '/')
break;
authority_len = (c - authority);
case PATH_START:
path = c;
state = PATH;
break;
case PATH:
break;
default:
GIT_ASSERT(!"unhandled state");
}
}
switch (state) {
case SCHEME:
path = given;
path_len = (c - path);
break;
case AUTHORITY_START:
break;
case AUTHORITY:
authority_len = (c - authority);
break;
case PATH_START:
break;
case PATH:
path_len = (c - path);
break;
default:
GIT_ASSERT(!"unhandled state");
}
if (authority_len &&
(error = url_parse_authority(&parser, authority, authority_len)) < 0)
goto done;
if (path_len &&
(error = url_parse_path(&parser, path, path_len)) < 0)
goto done;
error = url_parse_finalize(url, &parser);
done:
return error;
}
int git_net_url_parse_http(
git_net_url *url,
const char *given)
{
git_net_url_parser parser = GIT_NET_URL_PARSER_INIT;
const char *c, *authority, *path = NULL;
size_t authority_len = 0, path_len = 0;
int error;
if (git_net_str_is_url(given))
return git_net_url_parse(url, given);
memset(url, 0, sizeof(git_net_url));
for (c = authority = given; *c; c++) {
if (!path && *c == '/') {
authority_len = (c - authority);
path = c;
}
}
if (path)
path_len = (c - path);
else
authority_len = (c - authority);
parser.scheme = "http";
parser.scheme_len = 4;
parser.hierarchical = 1;
if (authority_len &&
(error = url_parse_authority(&parser, authority, authority_len)) < 0)
return error;
if (path_len &&
(error = url_parse_path(&parser, path, path_len)) < 0)
return error;
return url_parse_finalize(url, &parser);
}
static int scp_invalid(const char *message)
{
git_error_set(GIT_ERROR_NET, "invalid scp-style path: %s", message);
return GIT_EINVALIDSPEC;
}
static bool is_ipv6(const char *str)
{
const char *c;
size_t colons = 0;
if (*str++ != '[')
return false;
for (c = str; *c; c++) {
if (*c == ':')
colons++;
if (*c == ']')
return (colons > 1);
if (*c != ':' &&
(*c < '0' || *c > '9') &&
(*c < 'a' || *c > 'f') &&
(*c < 'A' || *c > 'F'))
return false;
}
return false;
}
static bool has_at(const char *str)
{
const char *c;
for (c = str; *c; c++) {
if (*c == '@')
return true;
if (*c == ':')
break;
}
return false;
}
int git_net_url_parse_scp(git_net_url *url, const char *given)
{
const char *default_port = default_port_for_scheme("ssh");
const char *c, *user, *host, *port = NULL, *path = NULL;
size_t user_len = 0, host_len = 0, port_len = 0;
unsigned short bracket = 0;
enum {
NONE,
USER,
HOST_START, HOST, HOST_END,
IPV6, IPV6_END,
PORT_START, PORT, PORT_END,
PATH_START
} state = NONE;
memset(url, 0, sizeof(git_net_url));
for (c = given; *c && !path; c++) {
switch (state) {
case NONE:
switch (*c) {
case '@':
return scp_invalid("unexpected '@'");
case ':':
return scp_invalid("unexpected ':'");
case '[':
if (is_ipv6(c)) {
state = IPV6;
host = c;
} else if (bracket++ > 1) {
return scp_invalid("unexpected '['");
}
break;
default:
if (has_at(c)) {
state = USER;
user = c;
} else {
state = HOST;
host = c;
}
break;
}
break;
case USER:
if (*c == '@') {
user_len = (c - user);
state = HOST_START;
}
break;
case HOST_START:
state = (*c == '[') ? IPV6 : HOST;
host = c;
break;
case HOST:
if (*c == ':') {
host_len = (c - host);
state = bracket ? PORT_START : PATH_START;
} else if (*c == ']') {
if (bracket-- == 0)
return scp_invalid("unexpected ']'");
host_len = (c - host);
state = HOST_END;
}
break;
case HOST_END:
if (*c != ':')
return scp_invalid("unexpected character after hostname");
state = PATH_START;
break;
case IPV6:
if (*c == ']')
state = IPV6_END;
break;
case IPV6_END:
if (*c != ':')
return scp_invalid("unexpected character after ipv6 address");
host_len = (c - host);
state = bracket ? PORT_START : PATH_START;
break;
case PORT_START:
port = c;
state = PORT;
break;
case PORT:
if (*c == ']') {
if (bracket-- == 0)
return scp_invalid("unexpected ']'");
port_len = c - port;
state = PORT_END;
}
break;
case PORT_END:
if (*c != ':')
return scp_invalid("unexpected character after ipv6 address");
state = PATH_START;
break;
case PATH_START:
path = c;
break;
default:
GIT_ASSERT(!"unhandled state");
}
}
if (!path)
return scp_invalid("path is required");
GIT_ERROR_CHECK_ALLOC(url->scheme = git__strdup("ssh"));
if (user_len)
GIT_ERROR_CHECK_ALLOC(url->username = git__strndup(user, user_len));
GIT_ASSERT(host_len);
GIT_ERROR_CHECK_ALLOC(url->host = git__strndup(host, host_len));
if (port_len) {
url->port_specified = 1;
GIT_ERROR_CHECK_ALLOC(url->port = git__strndup(port, port_len));
} else {
GIT_ERROR_CHECK_ALLOC(url->port = git__strdup(default_port));
}
GIT_ASSERT(path);
GIT_ERROR_CHECK_ALLOC(url->path = git__strdup(path));
return 0;
}
int git_net_url_parse_standard_or_scp(git_net_url *url, const char *given)
{
return git_net_str_is_url(given) ?
git_net_url_parse(url, given) :
git_net_url_parse_scp(url, given);
}
int git_net_url_joinpath(
git_net_url *out,
git_net_url *one,
const char *two)
{
git_str path = GIT_STR_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);
}
one_len = one->path ? strlen(one->path) : 0;
while (one_len && one->path[one_len - 1] == '/')
one_len--;
while (*two == '/') {
two++;
two_len--;
}
git_str_put(&path, one->path, one_len);
git_str_putc(&path, '/');
git_str_put(&path, two, two_len);
if (git_str_oom(&path))
return -1;
out->path = git_str_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;
}
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;
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 (truncate < 0 && git__suffixcmp(url->path, service_suffix) == 0)
truncate = path_len - full_suffix_len;
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,
bool allow_offsite,
const char *service_suffix)
{
git_net_url tmp = GIT_NET_URL_INIT;
int error = 0;
GIT_ASSERT(url);
GIT_ASSERT(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;
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 &&
!allow_offsite &&
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);
}
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);
}
bool git_net_url_is_default_port(git_net_url *url)
{
const char *default_port;
if (url->scheme && (default_port = default_port_for_scheme(url->scheme)) != NULL)
return (strcmp(url->port, default_port) == 0);
else
return false;
}
bool git_net_url_is_ipv6(git_net_url *url)
{
return (strchr(url->host, ':') != NULL);
}
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_str *buf, git_net_url *url)
{
GIT_ASSERT_ARG(url);
GIT_ASSERT_ARG(url->scheme);
GIT_ASSERT_ARG(url->host);
git_str_puts(buf, url->scheme);
git_str_puts(buf, "://");
if (url->username) {
git_str_puts(buf, url->username);
if (url->password) {
git_str_puts(buf, ":");
git_str_puts(buf, url->password);
}
git_str_putc(buf, '@');
}
git_str_puts(buf, url->host);
if (url->port && !git_net_url_is_default_port(url)) {
git_str_putc(buf, ':');
git_str_puts(buf, url->port);
}
git_str_puts(buf, url->path ? url->path : "/");
if (url->query) {
git_str_putc(buf, '?');
git_str_puts(buf, url->query);
}
return git_str_oom(buf) ? -1 : 0;
}
int git_net_url_fmt_path(git_str *buf, git_net_url *url)
{
git_str_puts(buf, url->path ? url->path : "/");
if (url->query) {
git_str_putc(buf, '?');
git_str_puts(buf, url->query);
}
return git_str_oom(buf) ? -1 : 0;
}
static bool matches_pattern(
git_net_url *url,
const char *pattern,
size_t pattern_len)
{
const char *domain, *port = NULL, *colon;
size_t host_len, domain_len, port_len = 0, wildcard = 0;
GIT_UNUSED(url);
GIT_UNUSED(pattern);
if (!pattern_len)
return false;
else if (pattern_len == 1 && pattern[0] == '*')
return true;
else if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.')
wildcard = 2;
else if (pattern[0] == '.')
wildcard = 1;
domain = pattern + wildcard;
domain_len = pattern_len - wildcard;
if ((colon = memchr(domain, ':', domain_len)) != NULL) {
domain_len = colon - domain;
port = colon + 1;
port_len = pattern_len - wildcard - domain_len - 1;
}
if (port_len && git__strlcmp(url->port, port, port_len) != 0)
return false;
if (!wildcard)
return !git__strlcmp(url->host, domain, domain_len);
if ((host_len = strlen(url->host)) < domain_len ||
memcmp(url->host + (host_len - domain_len), domain, domain_len))
return false;
if (host_len == domain_len)
return true;
return (url->host[host_len - domain_len - 1] == '.');
}
bool git_net_url_matches_pattern(git_net_url *url, const char *pattern)
{
return matches_pattern(url, pattern, strlen(pattern));
}
bool git_net_url_matches_pattern_list(
git_net_url *url,
const char *pattern_list)
{
const char *pattern, *pattern_end, *sep;
for (pattern = pattern_list;
pattern && *pattern;
pattern = sep ? sep + 1 : NULL) {
sep = strchr(pattern, ',');
pattern_end = sep ? sep : strchr(pattern, '\0');
if (matches_pattern(url, pattern, (pattern_end - pattern)))
return true;
}
return false;
}
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->fragment); url->fragment = NULL;
git__free(url->username); url->username = NULL;
git__free(url->password); url->password = NULL;
}