diff options
Diffstat (limited to 'connect.c')
-rw-r--r-- | connect.c | 1327 |
1 files changed, 1327 insertions, 0 deletions
diff --git a/connect.c b/connect.c new file mode 100644 index 000000000000..2778481264d0 --- /dev/null +++ b/connect.c @@ -0,0 +1,1327 @@ +#include "git-compat-util.h" +#include "cache.h" +#include "config.h" +#include "pkt-line.h" +#include "quote.h" +#include "refs.h" +#include "run-command.h" +#include "remote.h" +#include "connect.h" +#include "url.h" +#include "string-list.h" +#include "sha1-array.h" +#include "transport.h" +#include "strbuf.h" +#include "version.h" +#include "protocol.h" +#include "alias.h" + +static char *server_capabilities_v1; +static struct argv_array server_capabilities_v2 = ARGV_ARRAY_INIT; +static const char *parse_feature_value(const char *, const char *, int *); + +static int check_ref(const char *name, unsigned int flags) +{ + if (!flags) + return 1; + + if (!skip_prefix(name, "refs/", &name)) + return 0; + + /* REF_NORMAL means that we don't want the magic fake tag refs */ + if ((flags & REF_NORMAL) && check_refname_format(name, 0)) + return 0; + + /* REF_HEADS means that we want regular branch heads */ + if ((flags & REF_HEADS) && starts_with(name, "heads/")) + return 1; + + /* REF_TAGS means that we want tags */ + if ((flags & REF_TAGS) && starts_with(name, "tags/")) + return 1; + + /* All type bits clear means that we are ok with anything */ + return !(flags & ~REF_NORMAL); +} + +int check_ref_type(const struct ref *ref, int flags) +{ + return check_ref(ref->name, flags); +} + +static NORETURN void die_initial_contact(int unexpected) +{ + /* + * A hang-up after seeing some response from the other end + * means that it is unexpected, as we know the other end is + * willing to talk to us. A hang-up before seeing any + * response does not necessarily mean an ACL problem, though. + */ + if (unexpected) + die(_("the remote end hung up upon initial contact")); + else + die(_("Could not read from remote repository.\n\n" + "Please make sure you have the correct access rights\n" + "and the repository exists.")); +} + +/* Checks if the server supports the capability 'c' */ +int server_supports_v2(const char *c, int die_on_error) +{ + int i; + + for (i = 0; i < server_capabilities_v2.argc; i++) { + const char *out; + if (skip_prefix(server_capabilities_v2.argv[i], c, &out) && + (!*out || *out == '=')) + return 1; + } + + if (die_on_error) + die(_("server doesn't support '%s'"), c); + + return 0; +} + +int server_supports_feature(const char *c, const char *feature, + int die_on_error) +{ + int i; + + for (i = 0; i < server_capabilities_v2.argc; i++) { + const char *out; + if (skip_prefix(server_capabilities_v2.argv[i], c, &out) && + (!*out || *(out++) == '=')) { + if (parse_feature_request(out, feature)) + return 1; + else + break; + } + } + + if (die_on_error) + die(_("server doesn't support feature '%s'"), feature); + + return 0; +} + +static void process_capabilities_v2(struct packet_reader *reader) +{ + while (packet_reader_read(reader) == PACKET_READ_NORMAL) + argv_array_push(&server_capabilities_v2, reader->line); + + if (reader->status != PACKET_READ_FLUSH) + die(_("expected flush after capabilities")); +} + +enum protocol_version discover_version(struct packet_reader *reader) +{ + enum protocol_version version = protocol_unknown_version; + + /* + * Peek the first line of the server's response to + * determine the protocol version the server is speaking. + */ + switch (packet_reader_peek(reader)) { + case PACKET_READ_EOF: + die_initial_contact(0); + case PACKET_READ_FLUSH: + case PACKET_READ_DELIM: + version = protocol_v0; + break; + case PACKET_READ_NORMAL: + version = determine_protocol_version_client(reader->line); + break; + } + + switch (version) { + case protocol_v2: + process_capabilities_v2(reader); + break; + case protocol_v1: + /* Read the peeked version line */ + packet_reader_read(reader); + break; + case protocol_v0: + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + + return version; +} + +static void parse_one_symref_info(struct string_list *symref, const char *val, int len) +{ + char *sym, *target; + struct string_list_item *item; + + if (!len) + return; /* just "symref" */ + /* e.g. "symref=HEAD:refs/heads/master" */ + sym = xmemdupz(val, len); + target = strchr(sym, ':'); + if (!target) + /* just "symref=something" */ + goto reject; + *(target++) = '\0'; + if (check_refname_format(sym, REFNAME_ALLOW_ONELEVEL) || + check_refname_format(target, REFNAME_ALLOW_ONELEVEL)) + /* "symref=bogus:pair */ + goto reject; + item = string_list_append_nodup(symref, sym); + item->util = target; + return; +reject: + free(sym); + return; +} + +static void annotate_refs_with_symref_info(struct ref *ref) +{ + struct string_list symref = STRING_LIST_INIT_DUP; + const char *feature_list = server_capabilities_v1; + + while (feature_list) { + int len; + const char *val; + + val = parse_feature_value(feature_list, "symref", &len); + if (!val) + break; + parse_one_symref_info(&symref, val, len); + feature_list = val + 1; + } + string_list_sort(&symref); + + for (; ref; ref = ref->next) { + struct string_list_item *item; + item = string_list_lookup(&symref, ref->name); + if (!item) + continue; + ref->symref = xstrdup((char *)item->util); + } + string_list_clear(&symref, 0); +} + +static void process_capabilities(const char *line, int *len) +{ + int nul_location = strlen(line); + if (nul_location == *len) + return; + server_capabilities_v1 = xstrdup(line + nul_location + 1); + *len = nul_location; +} + +static int process_dummy_ref(const char *line) +{ + struct object_id oid; + const char *name; + + if (parse_oid_hex(line, &oid, &name)) + return 0; + if (*name != ' ') + return 0; + name++; + + return oideq(&null_oid, &oid) && !strcmp(name, "capabilities^{}"); +} + +static void check_no_capabilities(const char *line, int len) +{ + if (strlen(line) != len) + warning(_("ignoring capabilities after first line '%s'"), + line + strlen(line)); +} + +static int process_ref(const char *line, int len, struct ref ***list, + unsigned int flags, struct oid_array *extra_have) +{ + struct object_id old_oid; + const char *name; + + if (parse_oid_hex(line, &old_oid, &name)) + return 0; + if (*name != ' ') + return 0; + name++; + + if (extra_have && !strcmp(name, ".have")) { + oid_array_append(extra_have, &old_oid); + } else if (!strcmp(name, "capabilities^{}")) { + die(_("protocol error: unexpected capabilities^{}")); + } else if (check_ref(name, flags)) { + struct ref *ref = alloc_ref(name); + oidcpy(&ref->old_oid, &old_oid); + **list = ref; + *list = &ref->next; + } + check_no_capabilities(line, len); + return 1; +} + +static int process_shallow(const char *line, int len, + struct oid_array *shallow_points) +{ + const char *arg; + struct object_id old_oid; + + if (!skip_prefix(line, "shallow ", &arg)) + return 0; + + if (get_oid_hex(arg, &old_oid)) + die(_("protocol error: expected shallow sha-1, got '%s'"), arg); + if (!shallow_points) + die(_("repository on the other end cannot be shallow")); + oid_array_append(shallow_points, &old_oid); + check_no_capabilities(line, len); + return 1; +} + +enum get_remote_heads_state { + EXPECTING_FIRST_REF = 0, + EXPECTING_REF, + EXPECTING_SHALLOW, + EXPECTING_DONE, +}; + +/* + * Read all the refs from the other end + */ +struct ref **get_remote_heads(struct packet_reader *reader, + struct ref **list, unsigned int flags, + struct oid_array *extra_have, + struct oid_array *shallow_points) +{ + struct ref **orig_list = list; + int len = 0; + enum get_remote_heads_state state = EXPECTING_FIRST_REF; + + *list = NULL; + + while (state != EXPECTING_DONE) { + switch (packet_reader_read(reader)) { + case PACKET_READ_EOF: + die_initial_contact(1); + case PACKET_READ_NORMAL: + len = reader->pktlen; + break; + case PACKET_READ_FLUSH: + state = EXPECTING_DONE; + break; + case PACKET_READ_DELIM: + die(_("invalid packet")); + } + + switch (state) { + case EXPECTING_FIRST_REF: + process_capabilities(reader->line, &len); + if (process_dummy_ref(reader->line)) { + state = EXPECTING_SHALLOW; + break; + } + state = EXPECTING_REF; + /* fallthrough */ + case EXPECTING_REF: + if (process_ref(reader->line, len, &list, flags, extra_have)) + break; + state = EXPECTING_SHALLOW; + /* fallthrough */ + case EXPECTING_SHALLOW: + if (process_shallow(reader->line, len, shallow_points)) + break; + die(_("protocol error: unexpected '%s'"), reader->line); + case EXPECTING_DONE: + break; + } + } + + annotate_refs_with_symref_info(*orig_list); + + return list; +} + +/* Returns 1 when a valid ref has been added to `list`, 0 otherwise */ +static int process_ref_v2(const char *line, struct ref ***list) +{ + int ret = 1; + int i = 0; + struct object_id old_oid; + struct ref *ref; + struct string_list line_sections = STRING_LIST_INIT_DUP; + const char *end; + + /* + * Ref lines have a number of fields which are space deliminated. The + * first field is the OID of the ref. The second field is the ref + * name. Subsequent fields (symref-target and peeled) are optional and + * don't have a particular order. + */ + if (string_list_split(&line_sections, line, ' ', -1) < 2) { + ret = 0; + goto out; + } + + if (parse_oid_hex(line_sections.items[i++].string, &old_oid, &end) || + *end) { + ret = 0; + goto out; + } + + ref = alloc_ref(line_sections.items[i++].string); + + oidcpy(&ref->old_oid, &old_oid); + **list = ref; + *list = &ref->next; + + for (; i < line_sections.nr; i++) { + const char *arg = line_sections.items[i].string; + if (skip_prefix(arg, "symref-target:", &arg)) + ref->symref = xstrdup(arg); + + if (skip_prefix(arg, "peeled:", &arg)) { + struct object_id peeled_oid; + char *peeled_name; + struct ref *peeled; + if (parse_oid_hex(arg, &peeled_oid, &end) || *end) { + ret = 0; + goto out; + } + + peeled_name = xstrfmt("%s^{}", ref->name); + peeled = alloc_ref(peeled_name); + + oidcpy(&peeled->old_oid, &peeled_oid); + **list = peeled; + *list = &peeled->next; + + free(peeled_name); + } + } + +out: + string_list_clear(&line_sections, 0); + return ret; +} + +struct ref **get_remote_refs(int fd_out, struct packet_reader *reader, + struct ref **list, int for_push, + const struct argv_array *ref_prefixes, + const struct string_list *server_options) +{ + int i; + *list = NULL; + + if (server_supports_v2("ls-refs", 1)) + packet_write_fmt(fd_out, "command=ls-refs\n"); + + if (server_supports_v2("agent", 0)) + packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized()); + + if (server_options && server_options->nr && + server_supports_v2("server-option", 1)) + for (i = 0; i < server_options->nr; i++) + packet_write_fmt(fd_out, "server-option=%s", + server_options->items[i].string); + + packet_delim(fd_out); + /* When pushing we don't want to request the peeled tags */ + if (!for_push) + packet_write_fmt(fd_out, "peel\n"); + packet_write_fmt(fd_out, "symrefs\n"); + for (i = 0; ref_prefixes && i < ref_prefixes->argc; i++) { + packet_write_fmt(fd_out, "ref-prefix %s\n", + ref_prefixes->argv[i]); + } + packet_flush(fd_out); + + /* Process response from server */ + while (packet_reader_read(reader) == PACKET_READ_NORMAL) { + if (!process_ref_v2(reader->line, &list)) + die(_("invalid ls-refs response: %s"), reader->line); + } + + if (reader->status != PACKET_READ_FLUSH) + die(_("expected flush after ref listing")); + + return list; +} + +static const char *parse_feature_value(const char *feature_list, const char *feature, int *lenp) +{ + int len; + + if (!feature_list) + return NULL; + + len = strlen(feature); + while (*feature_list) { + const char *found = strstr(feature_list, feature); + if (!found) + return NULL; + if (feature_list == found || isspace(found[-1])) { + const char *value = found + len; + /* feature with no value (e.g., "thin-pack") */ + if (!*value || isspace(*value)) { + if (lenp) + *lenp = 0; + return value; + } + /* feature with a value (e.g., "agent=git/1.2.3") */ + else if (*value == '=') { + value++; + if (lenp) + *lenp = strcspn(value, " \t\n"); + return value; + } + /* + * otherwise we matched a substring of another feature; + * keep looking + */ + } + feature_list = found + 1; + } + return NULL; +} + +int parse_feature_request(const char *feature_list, const char *feature) +{ + return !!parse_feature_value(feature_list, feature, NULL); +} + +const char *server_feature_value(const char *feature, int *len) +{ + return parse_feature_value(server_capabilities_v1, feature, len); +} + +int server_supports(const char *feature) +{ + return !!server_feature_value(feature, NULL); +} + +enum protocol { + PROTO_LOCAL = 1, + PROTO_FILE, + PROTO_SSH, + PROTO_GIT +}; + +int url_is_local_not_ssh(const char *url) +{ + const char *colon = strchr(url, ':'); + const char *slash = strchr(url, '/'); + return !colon || (slash && slash < colon) || + has_dos_drive_prefix(url); +} + +static const char *prot_name(enum protocol protocol) +{ + switch (protocol) { + case PROTO_LOCAL: + case PROTO_FILE: + return "file"; + case PROTO_SSH: + return "ssh"; + case PROTO_GIT: + return "git"; + default: + return "unknown protocol"; + } +} + +static enum protocol get_protocol(const char *name) +{ + if (!strcmp(name, "ssh")) + return PROTO_SSH; + if (!strcmp(name, "git")) + return PROTO_GIT; + if (!strcmp(name, "git+ssh")) /* deprecated - do not use */ + return PROTO_SSH; + if (!strcmp(name, "ssh+git")) /* deprecated - do not use */ + return PROTO_SSH; + if (!strcmp(name, "file")) + return PROTO_FILE; + die(_("protocol '%s' is not supported"), name); +} + +static char *host_end(char **hoststart, int removebrackets) +{ + char *host = *hoststart; + char *end; + char *start = strstr(host, "@["); + if (start) + start++; /* Jump over '@' */ + else + start = host; + if (start[0] == '[') { + end = strchr(start + 1, ']'); + if (end) { + if (removebrackets) { + *end = 0; + memmove(start, start + 1, end - start); + end++; + } + } else + end = host; + } else + end = host; + return end; +} + +#define STR_(s) # s +#define STR(s) STR_(s) + +static void get_host_and_port(char **host, const char **port) +{ + char *colon, *end; + end = host_end(host, 1); + colon = strchr(end, ':'); + if (colon) { + long portnr = strtol(colon + 1, &end, 10); + if (end != colon + 1 && *end == '\0' && 0 <= portnr && portnr < 65536) { + *colon = 0; + *port = colon + 1; + } else if (!colon[1]) { + *colon = 0; + } + } +} + +static void enable_keepalive(int sockfd) +{ + int ka = 1; + + if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &ka, sizeof(ka)) < 0) + error_errno(_("unable to set SO_KEEPALIVE on socket")); +} + +#ifndef NO_IPV6 + +static const char *ai_name(const struct addrinfo *ai) +{ + static char addr[NI_MAXHOST]; + if (getnameinfo(ai->ai_addr, ai->ai_addrlen, addr, sizeof(addr), NULL, 0, + NI_NUMERICHOST) != 0) + xsnprintf(addr, sizeof(addr), "(unknown)"); + + return addr; +} + +/* + * Returns a connected socket() fd, or else die()s. + */ +static int git_tcp_connect_sock(char *host, int flags) +{ + struct strbuf error_message = STRBUF_INIT; + int sockfd = -1; + const char *port = STR(DEFAULT_GIT_PORT); + struct addrinfo hints, *ai0, *ai; + int gai; + int cnt = 0; + + get_host_and_port(&host, &port); + if (!*port) + port = "<none>"; + + memset(&hints, 0, sizeof(hints)); + if (flags & CONNECT_IPV4) + hints.ai_family = AF_INET; + else if (flags & CONNECT_IPV6) + hints.ai_family = AF_INET6; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + if (flags & CONNECT_VERBOSE) + fprintf(stderr, _("Looking up %s ... "), host); + + gai = getaddrinfo(host, port, &hints, &ai); + if (gai) + die(_("unable to look up %s (port %s) (%s)"), host, port, gai_strerror(gai)); + + if (flags & CONNECT_VERBOSE) + /* TRANSLATORS: this is the end of "Looking up %s ... " */ + fprintf(stderr, _("done.\nConnecting to %s (port %s) ... "), host, port); + + for (ai0 = ai; ai; ai = ai->ai_next, cnt++) { + sockfd = socket(ai->ai_family, + ai->ai_socktype, ai->ai_protocol); + if ((sockfd < 0) || + (connect(sockfd, ai->ai_addr, ai->ai_addrlen) < 0)) { + strbuf_addf(&error_message, "%s[%d: %s]: errno=%s\n", + host, cnt, ai_name(ai), strerror(errno)); + if (0 <= sockfd) + close(sockfd); + sockfd = -1; + continue; + } + if (flags & CONNECT_VERBOSE) + fprintf(stderr, "%s ", ai_name(ai)); + break; + } + + freeaddrinfo(ai0); + + if (sockfd < 0) + die(_("unable to connect to %s:\n%s"), host, error_message.buf); + + enable_keepalive(sockfd); + + if (flags & CONNECT_VERBOSE) + /* TRANSLATORS: this is the end of "Connecting to %s (port %s) ... " */ + fprintf_ln(stderr, _("done.")); + + strbuf_release(&error_message); + + return sockfd; +} + +#else /* NO_IPV6 */ + +/* + * Returns a connected socket() fd, or else die()s. + */ +static int git_tcp_connect_sock(char *host, int flags) +{ + struct strbuf error_message = STRBUF_INIT; + int sockfd = -1; + const char *port = STR(DEFAULT_GIT_PORT); + char *ep; + struct hostent *he; + struct sockaddr_in sa; + char **ap; + unsigned int nport; + int cnt; + + get_host_and_port(&host, &port); + + if (flags & CONNECT_VERBOSE) + fprintf(stderr, _("Looking up %s ... "), host); + + he = gethostbyname(host); + if (!he) + die(_("unable to look up %s (%s)"), host, hstrerror(h_errno)); + nport = strtoul(port, &ep, 10); + if ( ep == port || *ep ) { + /* Not numeric */ + struct servent *se = getservbyname(port,"tcp"); + if ( !se ) + die(_("unknown port %s"), port); + nport = se->s_port; + } + + if (flags & CONNECT_VERBOSE) + /* TRANSLATORS: this is the end of "Looking up %s ... " */ + fprintf(stderr, _("done.\nConnecting to %s (port %s) ... "), host, port); + + for (cnt = 0, ap = he->h_addr_list; *ap; ap++, cnt++) { + memset(&sa, 0, sizeof sa); + sa.sin_family = he->h_addrtype; + sa.sin_port = htons(nport); + memcpy(&sa.sin_addr, *ap, he->h_length); + + sockfd = socket(he->h_addrtype, SOCK_STREAM, 0); + if ((sockfd < 0) || + connect(sockfd, (struct sockaddr *)&sa, sizeof sa) < 0) { + strbuf_addf(&error_message, "%s[%d: %s]: errno=%s\n", + host, + cnt, + inet_ntoa(*(struct in_addr *)&sa.sin_addr), + strerror(errno)); + if (0 <= sockfd) + close(sockfd); + sockfd = -1; + continue; + } + if (flags & CONNECT_VERBOSE) + fprintf(stderr, "%s ", + inet_ntoa(*(struct in_addr *)&sa.sin_addr)); + break; + } + + if (sockfd < 0) + die(_("unable to connect to %s:\n%s"), host, error_message.buf); + + enable_keepalive(sockfd); + + if (flags & CONNECT_VERBOSE) + /* TRANSLATORS: this is the end of "Connecting to %s (port %s) ... " */ + fprintf_ln(stderr, _("done.")); + + return sockfd; +} + +#endif /* NO_IPV6 */ + + +/* + * Dummy child_process returned by git_connect() if the transport protocol + * does not need fork(2). + */ +static struct child_process no_fork = CHILD_PROCESS_INIT; + +int git_connection_is_socket(struct child_process *conn) +{ + return conn == &no_fork; +} + +static struct child_process *git_tcp_connect(int fd[2], char *host, int flags) +{ + int sockfd = git_tcp_connect_sock(host, flags); + + fd[0] = sockfd; + fd[1] = dup(sockfd); + + return &no_fork; +} + + +static char *git_proxy_command; + +static int git_proxy_command_options(const char *var, const char *value, + void *cb) +{ + if (!strcmp(var, "core.gitproxy")) { + const char *for_pos; + int matchlen = -1; + int hostlen; + const char *rhost_name = cb; + int rhost_len = strlen(rhost_name); + + if (git_proxy_command) + return 0; + if (!value) + return config_error_nonbool(var); + /* [core] + * ;# matches www.kernel.org as well + * gitproxy = netcatter-1 for kernel.org + * gitproxy = netcatter-2 for sample.xz + * gitproxy = netcatter-default + */ + for_pos = strstr(value, " for "); + if (!for_pos) + /* matches everybody */ + matchlen = strlen(value); + else { + hostlen = strlen(for_pos + 5); + if (rhost_len < hostlen) + matchlen = -1; + else if (!strncmp(for_pos + 5, + rhost_name + rhost_len - hostlen, + hostlen) && + ((rhost_len == hostlen) || + rhost_name[rhost_len - hostlen -1] == '.')) + matchlen = for_pos - value; + else + matchlen = -1; + } + if (0 <= matchlen) { + /* core.gitproxy = none for kernel.org */ + if (matchlen == 4 && + !memcmp(value, "none", 4)) + matchlen = 0; + git_proxy_command = xmemdupz(value, matchlen); + } + return 0; + } + + return git_default_config(var, value, cb); +} + +static int git_use_proxy(const char *host) +{ + git_proxy_command = getenv("GIT_PROXY_COMMAND"); + git_config(git_proxy_command_options, (void*)host); + return (git_proxy_command && *git_proxy_command); +} + +static struct child_process *git_proxy_connect(int fd[2], char *host) +{ + const char *port = STR(DEFAULT_GIT_PORT); + struct child_process *proxy; + + get_host_and_port(&host, &port); + + if (looks_like_command_line_option(host)) + die(_("strange hostname '%s' blocked"), host); + if (looks_like_command_line_option(port)) + die(_("strange port '%s' blocked"), port); + + proxy = xmalloc(sizeof(*proxy)); + child_process_init(proxy); + argv_array_push(&proxy->args, git_proxy_command); + argv_array_push(&proxy->args, host); + argv_array_push(&proxy->args, port); + proxy->in = -1; + proxy->out = -1; + if (start_command(proxy)) + die(_("cannot start proxy %s"), git_proxy_command); + fd[0] = proxy->out; /* read from proxy stdout */ + fd[1] = proxy->in; /* write to proxy stdin */ + return proxy; +} + +static char *get_port(char *host) +{ + char *end; + char *p = strchr(host, ':'); + + if (p) { + long port = strtol(p + 1, &end, 10); + if (end != p + 1 && *end == '\0' && 0 <= port && port < 65536) { + *p = '\0'; + return p+1; + } + } + + return NULL; +} + +/* + * Extract protocol and relevant parts from the specified connection URL. + * The caller must free() the returned strings. + */ +static enum protocol parse_connect_url(const char *url_orig, char **ret_host, + char **ret_path) +{ + char *url; + char *host, *path; + char *end; + int separator = '/'; + enum protocol protocol = PROTO_LOCAL; + + if (is_url(url_orig)) + url = url_decode(url_orig); + else + url = xstrdup(url_orig); + + host = strstr(url, "://"); + if (host) { + *host = '\0'; + protocol = get_protocol(url); + host += 3; + } else { + host = url; + if (!url_is_local_not_ssh(url)) { + protocol = PROTO_SSH; + separator = ':'; + } + } + + /* + * Don't do destructive transforms as protocol code does + * '[]' unwrapping in get_host_and_port() + */ + end = host_end(&host, 0); + + if (protocol == PROTO_LOCAL) + path = end; + else if (protocol == PROTO_FILE && has_dos_drive_prefix(end)) + path = end; /* "file://$(pwd)" may be "file://C:/projects/repo" */ + else + path = strchr(end, separator); + + if (!path || !*path) + die(_("no path specified; see 'git help pull' for valid url syntax")); + + /* + * null-terminate hostname and point path to ~ for URL's like this: + * ssh://host.xz/~user/repo + */ + + end = path; /* Need to \0 terminate host here */ + if (separator == ':') + path++; /* path starts after ':' */ + if (protocol == PROTO_GIT || protocol == PROTO_SSH) { + if (path[1] == '~') + path++; + } + + path = xstrdup(path); + *end = '\0'; + + *ret_host = xstrdup(host); + *ret_path = path; + free(url); + return protocol; +} + +static const char *get_ssh_command(void) +{ + const char *ssh; + + if ((ssh = getenv("GIT_SSH_COMMAND"))) + return ssh; + + if (!git_config_get_string_const("core.sshcommand", &ssh)) + return ssh; + + return NULL; +} + +enum ssh_variant { + VARIANT_AUTO, + VARIANT_SIMPLE, + VARIANT_SSH, + VARIANT_PLINK, + VARIANT_PUTTY, + VARIANT_TORTOISEPLINK, +}; + +static void override_ssh_variant(enum ssh_variant *ssh_variant) +{ + const char *variant = getenv("GIT_SSH_VARIANT"); + + if (!variant && git_config_get_string_const("ssh.variant", &variant)) + return; + + if (!strcmp(variant, "auto")) + *ssh_variant = VARIANT_AUTO; + else if (!strcmp(variant, "plink")) + *ssh_variant = VARIANT_PLINK; + else if (!strcmp(variant, "putty")) + *ssh_variant = VARIANT_PUTTY; + else if (!strcmp(variant, "tortoiseplink")) + *ssh_variant = VARIANT_TORTOISEPLINK; + else if (!strcmp(variant, "simple")) + *ssh_variant = VARIANT_SIMPLE; + else + *ssh_variant = VARIANT_SSH; +} + +static enum ssh_variant determine_ssh_variant(const char *ssh_command, + int is_cmdline) +{ + enum ssh_variant ssh_variant = VARIANT_AUTO; + const char *variant; + char *p = NULL; + + override_ssh_variant(&ssh_variant); + + if (ssh_variant != VARIANT_AUTO) + return ssh_variant; + + if (!is_cmdline) { + p = xstrdup(ssh_command); + variant = basename(p); + } else { + const char **ssh_argv; + + p = xstrdup(ssh_command); + if (split_cmdline(p, &ssh_argv) > 0) { + variant = basename((char *)ssh_argv[0]); + /* + * At this point, variant points into the buffer + * referenced by p, hence we do not need ssh_argv + * any longer. + */ + free(ssh_argv); + } else { + free(p); + return ssh_variant; + } + } + + if (!strcasecmp(variant, "ssh") || + !strcasecmp(variant, "ssh.exe")) + ssh_variant = VARIANT_SSH; + else if (!strcasecmp(variant, "plink") || + !strcasecmp(variant, "plink.exe")) + ssh_variant = VARIANT_PLINK; + else if (!strcasecmp(variant, "tortoiseplink") || + !strcasecmp(variant, "tortoiseplink.exe")) + ssh_variant = VARIANT_TORTOISEPLINK; + + free(p); + return ssh_variant; +} + +/* + * Open a connection using Git's native protocol. + * + * The caller is responsible for freeing hostandport, but this function may + * modify it (for example, to truncate it to remove the port part). + */ +static struct child_process *git_connect_git(int fd[2], char *hostandport, + const char *path, const char *prog, + enum protocol_version version, + int flags) +{ + struct child_process *conn; + struct strbuf request = STRBUF_INIT; + /* + * Set up virtual host information based on where we will + * connect, unless the user has overridden us in + * the environment. + */ + char *target_host = getenv("GIT_OVERRIDE_VIRTUAL_HOST"); + if (target_host) + target_host = xstrdup(target_host); + else + target_host = xstrdup(hostandport); + + transport_check_allowed("git"); + + /* + * These underlying connection commands die() if they + * cannot connect. + */ + if (git_use_proxy(hostandport)) + conn = git_proxy_connect(fd, hostandport); + else + conn = git_tcp_connect(fd, hostandport, flags); + /* + * Separate original protocol components prog and path + * from extended host header with a NUL byte. + * + * Note: Do not add any other headers here! Doing so + * will cause older git-daemon servers to crash. + */ + strbuf_addf(&request, + "%s %s%chost=%s%c", + prog, path, 0, + target_host, 0); + + /* If using a new version put that stuff here after a second null byte */ + if (version > 0) { + strbuf_addch(&request, '\0'); + strbuf_addf(&request, "version=%d%c", + version, '\0'); + } + + packet_write(fd[1], request.buf, request.len); + + free(target_host); + strbuf_release(&request); + return conn; +} + +/* + * Append the appropriate environment variables to `env` and options to + * `args` for running ssh in Git's SSH-tunneled transport. + */ +static void push_ssh_options(struct argv_array *args, struct argv_array *env, + enum ssh_variant variant, const char *port, + enum protocol_version version, int flags) +{ + if (variant == VARIANT_SSH && + version > 0) { + argv_array_push(args, "-o"); + argv_array_push(args, "SendEnv=" GIT_PROTOCOL_ENVIRONMENT); + argv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT "=version=%d", + version); + } + + if (flags & CONNECT_IPV4) { + switch (variant) { + case VARIANT_AUTO: + BUG("VARIANT_AUTO passed to push_ssh_options"); + case VARIANT_SIMPLE: + die(_("ssh variant 'simple' does not support -4")); + case VARIANT_SSH: + case VARIANT_PLINK: + case VARIANT_PUTTY: + case VARIANT_TORTOISEPLINK: + argv_array_push(args, "-4"); + } + } else if (flags & CONNECT_IPV6) { + switch (variant) { + case VARIANT_AUTO: + BUG("VARIANT_AUTO passed to push_ssh_options"); + case VARIANT_SIMPLE: + die(_("ssh variant 'simple' does not support -6")); + case VARIANT_SSH: + case VARIANT_PLINK: + case VARIANT_PUTTY: + case VARIANT_TORTOISEPLINK: + argv_array_push(args, "-6"); + } + } + + if (variant == VARIANT_TORTOISEPLINK) + argv_array_push(args, "-batch"); + + if (port) { + switch (variant) { + case VARIANT_AUTO: + BUG("VARIANT_AUTO passed to push_ssh_options"); + case VARIANT_SIMPLE: + die(_("ssh variant 'simple' does not support setting port")); + case VARIANT_SSH: + argv_array_push(args, "-p"); + break; + case VARIANT_PLINK: + case VARIANT_PUTTY: + case VARIANT_TORTOISEPLINK: + argv_array_push(args, "-P"); + } + + argv_array_push(args, port); + } +} + +/* Prepare a child_process for use by Git's SSH-tunneled transport. */ +static void fill_ssh_args(struct child_process *conn, const char *ssh_host, + const char *port, enum protocol_version version, + int flags) +{ + const char *ssh; + enum ssh_variant variant; + + if (looks_like_command_line_option(ssh_host)) + die(_("strange hostname '%s' blocked"), ssh_host); + + ssh = get_ssh_command(); + if (ssh) { + variant = determine_ssh_variant(ssh, 1); + } else { + /* + * GIT_SSH is the no-shell version of + * GIT_SSH_COMMAND (and must remain so for + * historical compatibility). + */ + conn->use_shell = 0; + + ssh = getenv("GIT_SSH"); + if (!ssh) + ssh = "ssh"; + variant = determine_ssh_variant(ssh, 0); + } + + if (variant == VARIANT_AUTO) { + struct child_process detect = CHILD_PROCESS_INIT; + + detect.use_shell = conn->use_shell; + detect.no_stdin = detect.no_stdout = detect.no_stderr = 1; + + argv_array_push(&detect.args, ssh); + argv_array_push(&detect.args, "-G"); + push_ssh_options(&detect.args, &detect.env_array, + VARIANT_SSH, port, version, flags); + argv_array_push(&detect.args, ssh_host); + + variant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH; + } + + argv_array_push(&conn->args, ssh); + push_ssh_options(&conn->args, &conn->env_array, variant, port, version, flags); + argv_array_push(&conn->args, ssh_host); +} + +/* + * This returns the dummy child_process `no_fork` if the transport protocol + * does not need fork(2), or a struct child_process object if it does. Once + * done, finish the connection with finish_connect() with the value returned + * from this function (it is safe to call finish_connect() with NULL to + * support the former case). + * + * If it returns, the connect is successful; it just dies on errors (this + * will hopefully be changed in a libification effort, to return NULL when + * the connection failed). + */ +struct child_process *git_connect(int fd[2], const char *url, + const char *prog, int flags) +{ + char *hostandport, *path; + struct child_process *conn; + enum protocol protocol; + enum protocol_version version = get_protocol_version_config(); + + /* + * NEEDSWORK: If we are trying to use protocol v2 and we are planning + * to perform a push, then fallback to v0 since the client doesn't know + * how to push yet using v2. + */ + if (version == protocol_v2 && !strcmp("git-receive-pack", prog)) + version = protocol_v0; + + /* Without this we cannot rely on waitpid() to tell + * what happened to our children. + */ + signal(SIGCHLD, SIG_DFL); + + protocol = parse_connect_url(url, &hostandport, &path); + if ((flags & CONNECT_DIAG_URL) && (protocol != PROTO_SSH)) { + printf("Diag: url=%s\n", url ? url : "NULL"); + printf("Diag: protocol=%s\n", prot_name(protocol)); + printf("Diag: hostandport=%s\n", hostandport ? hostandport : "NULL"); + printf("Diag: path=%s\n", path ? path : "NULL"); + conn = NULL; + } else if (protocol == PROTO_GIT) { + conn = git_connect_git(fd, hostandport, path, prog, version, flags); + conn->trace2_child_class = "transport/git"; + } else { + struct strbuf cmd = STRBUF_INIT; + const char *const *var; + + conn = xmalloc(sizeof(*conn)); + child_process_init(conn); + + if (looks_like_command_line_option(path)) + die(_("strange pathname '%s' blocked"), path); + + strbuf_addstr(&cmd, prog); + strbuf_addch(&cmd, ' '); + sq_quote_buf(&cmd, path); + + /* remove repo-local variables from the environment */ + for (var = local_repo_env; *var; var++) + argv_array_push(&conn->env_array, *var); + + conn->use_shell = 1; + conn->in = conn->out = -1; + if (protocol == PROTO_SSH) { + char *ssh_host = hostandport; + const char *port = NULL; + transport_check_allowed("ssh"); + get_host_and_port(&ssh_host, &port); + + if (!port) + port = get_port(ssh_host); + + if (flags & CONNECT_DIAG_URL) { + printf("Diag: url=%s\n", url ? url : "NULL"); + printf("Diag: protocol=%s\n", prot_name(protocol)); + printf("Diag: userandhost=%s\n", ssh_host ? ssh_host : "NULL"); + printf("Diag: port=%s\n", port ? port : "NONE"); + printf("Diag: path=%s\n", path ? path : "NULL"); + + free(hostandport); + free(path); + free(conn); + strbuf_release(&cmd); + return NULL; + } + conn->trace2_child_class = "transport/ssh"; + fill_ssh_args(conn, ssh_host, port, version, flags); + } else { + transport_check_allowed("file"); + conn->trace2_child_class = "transport/file"; + if (version > 0) { + argv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT "=version=%d", + version); + } + } + argv_array_push(&conn->args, cmd.buf); + + if (start_command(conn)) + die(_("unable to fork")); + + fd[0] = conn->out; /* read from child's stdout */ + fd[1] = conn->in; /* write to child's stdin */ + strbuf_release(&cmd); + } + free(hostandport); + free(path); + return conn; +} + +int finish_connect(struct child_process *conn) +{ + int code; + if (!conn || git_connection_is_socket(conn)) + return 0; + + code = finish_command(conn); + free(conn); + return code; +} |