diff options
Diffstat (limited to 'src/transports')
-rw-r--r-- | src/transports/cred.c | 60 | ||||
-rw-r--r-- | src/transports/cred_helpers.c | 49 | ||||
-rw-r--r-- | src/transports/git.c | 544 | ||||
-rw-r--r-- | src/transports/http.c | 1203 | ||||
-rw-r--r-- | src/transports/local.c | 514 | ||||
-rw-r--r-- | src/transports/smart.c | 345 | ||||
-rw-r--r-- | src/transports/smart.h | 179 | ||||
-rw-r--r-- | src/transports/smart_pkt.c | 546 | ||||
-rw-r--r-- | src/transports/smart_protocol.c | 856 | ||||
-rw-r--r-- | src/transports/winhttp.c | 1136 |
10 files changed, 4574 insertions, 858 deletions
diff --git a/src/transports/cred.c b/src/transports/cred.c new file mode 100644 index 000000000..ecb026062 --- /dev/null +++ b/src/transports/cred.c @@ -0,0 +1,60 @@ +/* + * 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 "git2.h" +#include "smart.h" +#include "git2/cred_helpers.h" + +static void plaintext_free(struct git_cred *cred) +{ + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + size_t pass_len = strlen(c->password); + + git__free(c->username); + + /* Zero the memory which previously held the password */ + memset(c->password, 0x0, pass_len); + git__free(c->password); + + memset(c, 0, sizeof(*c)); + + git__free(c); +} + +int git_cred_userpass_plaintext_new( + git_cred **cred, + const char *username, + const char *password) +{ + git_cred_userpass_plaintext *c; + + if (!cred) + return -1; + + c = git__malloc(sizeof(git_cred_userpass_plaintext)); + GITERR_CHECK_ALLOC(c); + + c->parent.credtype = GIT_CREDTYPE_USERPASS_PLAINTEXT; + c->parent.free = plaintext_free; + c->username = git__strdup(username); + + if (!c->username) { + git__free(c); + return -1; + } + + c->password = git__strdup(password); + + if (!c->password) { + git__free(c->username); + git__free(c); + return -1; + } + + *cred = &c->parent; + return 0; +} diff --git a/src/transports/cred_helpers.c b/src/transports/cred_helpers.c new file mode 100644 index 000000000..d420e3e3c --- /dev/null +++ b/src/transports/cred_helpers.c @@ -0,0 +1,49 @@ +/* + * 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 "common.h" +#include "git2/cred_helpers.h" + +int git_cred_userpass( + git_cred **cred, + const char *url, + const char *user_from_url, + unsigned int allowed_types, + void *payload) +{ + git_cred_userpass_payload *userpass = (git_cred_userpass_payload*)payload; + const char *effective_username = NULL; + + GIT_UNUSED(url); + + if (!userpass || !userpass->password) return -1; + + /* Username resolution: a username can be passed with the URL, the + * credentials payload, or both. Here's what we do. Note that if we get + * this far, we know that any password the url may contain has already + * failed at least once, so we ignore it. + * + * | Payload | URL | Used | + * +-------------+----------+-----------+ + * | yes | no | payload | + * | yes | yes | payload | + * | no | yes | url | + * | no | no | FAIL | + */ + if (userpass->username) + effective_username = userpass->username; + else if (user_from_url) + effective_username = user_from_url; + else + return -1; + + if ((GIT_CREDTYPE_USERPASS_PLAINTEXT & allowed_types) == 0 || + git_cred_userpass_plaintext_new(cred, effective_username, userpass->password) < 0) + return -1; + + return 0; +} diff --git a/src/transports/git.c b/src/transports/git.c index 5baa810f0..3a0b86345 100644 --- a/src/transports/git.c +++ b/src/transports/git.c @@ -1,50 +1,42 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * 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 "git2/net.h" -#include "git2/common.h" -#include "git2/types.h" -#include "git2/errors.h" -#include "git2/net.h" -#include "git2/revwalk.h" - -#include "vector.h" -#include "transport.h" -#include "pkt.h" -#include "common.h" +#include "git2.h" +#include "buffer.h" #include "netops.h" -#include "filebuf.h" -#include "repository.h" -#include "fetch.h" -#include "protocol.h" + +#define OWNING_SUBTRANSPORT(s) ((git_subtransport *)(s)->parent.subtransport) + +static const char prefix_git[] = "git://"; +static const char cmd_uploadpack[] = "git-upload-pack"; +static const char cmd_receivepack[] = "git-receive-pack"; typedef struct { - git_transport parent; - git_protocol proto; - GIT_SOCKET socket; - git_vector refs; - git_remote_head **heads; - git_transport_caps caps; - char buff[1024]; - gitno_buffer buf; -#ifdef GIT_WIN32 - WSADATA wsd; -#endif -} transport_git; + git_smart_subtransport_stream parent; + gitno_socket socket; + const char *cmd; + char *url; + unsigned sent_command : 1; +} git_stream; + +typedef struct { + git_smart_subtransport parent; + git_transport *owner; + git_stream *current_stream; +} git_subtransport; /* - * Create a git procol request. + * Create a git protocol request. * * For example: 0035git-upload-pack /libgit2/libgit2\0host=github.com\0 */ static int gen_proto(git_buf *request, const char *cmd, const char *url) { char *delim, *repo; - char default_command[] = "git-upload-pack"; char host[] = "host="; size_t len; @@ -60,9 +52,6 @@ static int gen_proto(git_buf *request, const char *cmd, const char *url) if (delim == NULL) delim = strchr(url, '/'); - if (cmd == NULL) - cmd = default_command; - len = 4 + strlen(cmd) + 1 + strlen(repo) + 1 + strlen(host) + (delim - url) + 1; git_buf_grow(request, len); @@ -77,402 +66,287 @@ static int gen_proto(git_buf *request, const char *cmd, const char *url) return 0; } -static int send_request(GIT_SOCKET s, const char *cmd, const char *url) +static int send_command(git_stream *s) { int error; git_buf request = GIT_BUF_INIT; - error = gen_proto(&request, cmd, url); + error = gen_proto(&request, s->cmd, s->url); if (error < 0) goto cleanup; - error = gitno_send(s, request.ptr, request.size, 0); + /* It looks like negative values are errors here, and positive values + * are the number of bytes sent. */ + error = gitno_send(&s->socket, request.ptr, request.size, 0); + + if (error >= 0) + s->sent_command = 1; cleanup: git_buf_free(&request); return error; } -/* - * Parse the URL and connect to a server, storing the socket in - * out. For convenience this also takes care of asking for the remote - * refs - */ -static int do_connect(transport_git *t, const char *url) +static int git_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) { - char *host, *port; - const char prefix[] = "git://"; - int error; - - t->socket = INVALID_SOCKET; + git_stream *s = (git_stream *)stream; + gitno_buffer buf; - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); + *bytes_read = 0; - if (gitno_extract_host_and_port(&host, &port, url, GIT_DEFAULT_PORT) < 0) + if (!s->sent_command && send_command(s) < 0) return -1; - if ((error = gitno_connect(&t->socket, host, port)) == 0) { - error = send_request(t->socket, NULL, url); - } - - git__free(host); - git__free(port); - - if (error < 0 && t->socket != INVALID_SOCKET) { - gitno_close(t->socket); - t->socket = INVALID_SOCKET; - } + gitno_buffer_setup(&s->socket, &buf, buffer, buf_size); - if (t->socket == INVALID_SOCKET) { - giterr_set(GITERR_NET, "Failed to connect to the host"); + if (gitno_recv(&buf) < 0) return -1; - } + + *bytes_read = buf.offset; return 0; } -/* - * Read from the socket and store the references in the vector - */ -static int store_refs(transport_git *t) +static int git_stream_write( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - gitno_buffer *buf = &t->buf; - int ret = 0; - - while (1) { - if ((ret = gitno_recv(buf)) < 0) - return -1; - if (ret == 0) /* Orderly shutdown, so exit */ - return 0; - - ret = git_protocol_store_refs(&t->proto, buf->data, buf->offset); - if (ret == GIT_EBUFS) { - gitno_consume_n(buf, buf->len); - continue; - } - - if (ret < 0) - return ret; - - gitno_consume_n(buf, buf->offset); - - if (t->proto.flush) { /* No more refs */ - t->proto.flush = 0; - return 0; - } - } + git_stream *s = (git_stream *)stream; + + if (!s->sent_command && send_command(s) < 0) + return -1; + + return gitno_send(&s->socket, buffer, len, 0); } -static int detect_caps(transport_git *t) +static void git_stream_free(git_smart_subtransport_stream *stream) { - git_vector *refs = &t->refs; - git_pkt_ref *pkt; - git_transport_caps *caps = &t->caps; - const char *ptr; - - pkt = git_vector_get(refs, 0); - /* No refs or capabilites, odd but not a problem */ - if (pkt == NULL || pkt->capabilities == NULL) - return 0; + git_stream *s = (git_stream *)stream; + git_subtransport *t = OWNING_SUBTRANSPORT(s); + int ret; - ptr = pkt->capabilities; - while (ptr != NULL && *ptr != '\0') { - if (*ptr == ' ') - ptr++; + GIT_UNUSED(ret); - if(!git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { - caps->common = caps->ofs_delta = 1; - ptr += strlen(GIT_CAP_OFS_DELTA); - continue; - } + t->current_stream = NULL; - /* We don't know this capability, so skip it */ - ptr = strchr(ptr, ' '); + if (s->socket.socket) { + ret = gitno_close(&s->socket); + assert(!ret); } - return 0; + git__free(s->url); + git__free(s); } -/* - * Since this is a network connection, we need to parse and store the - * pkt-lines at this stage and keep them there. - */ -static int git_connect(git_transport *transport, int direction) +static int git_stream_alloc( + git_subtransport *t, + const char *url, + const char *cmd, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - - if (direction == GIT_DIR_PUSH) { - giterr_set(GITERR_NET, "Pushing over git:// is not supported"); - return -1; - } + git_stream *s; - t->parent.direction = direction; - if (git_vector_init(&t->refs, 16, NULL) < 0) + if (!stream) return -1; - /* Connect and ask for the refs */ - if (do_connect(t, transport->url) < 0) - goto cleanup; + s = git__calloc(sizeof(git_stream), 1); + GITERR_CHECK_ALLOC(s); - gitno_buffer_setup(&t->buf, t->buff, sizeof(t->buff), t->socket); + s->parent.subtransport = &t->parent; + s->parent.read = git_stream_read; + s->parent.write = git_stream_write; + s->parent.free = git_stream_free; - t->parent.connected = 1; - if (store_refs(t) < 0) - goto cleanup; + s->cmd = cmd; + s->url = git__strdup(url); - if (detect_caps(t) < 0) - goto cleanup; + if (!s->url) { + git__free(s); + return -1; + } + *stream = &s->parent; return 0; -cleanup: - git_vector_free(&t->refs); - return -1; } -static int git_ls(git_transport *transport, git_headlist_cb list_cb, void *opaque) +static int _git_uploadpack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_pkt *p = NULL; + char *host, *port, *user=NULL, *pass=NULL; + git_stream *s; + + *stream = NULL; - git_vector_foreach(refs, i, p) { - git_pkt_ref *pkt = NULL; + if (!git__prefixcmp(url, prefix_git)) + url += strlen(prefix_git); - if (p->type != GIT_PKT_REF) - continue; + if (git_stream_alloc(t, url, cmd_uploadpack, stream) < 0) + return -1; - pkt = (git_pkt_ref *)p; + s = (git_stream *)*stream; - if (list_cb(&pkt->head, opaque) < 0) { - giterr_set(GITERR_NET, "User callback returned error"); - return -1; - } - } + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, GIT_DEFAULT_PORT) < 0) + goto on_error; + if (gitno_connect(&s->socket, host, port, 0) < 0) + goto on_error; + + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); return 0; + +on_error: + if (*stream) + git_stream_free(*stream); + + git__free(host); + git__free(port); + return -1; } -/* Wait until we get an ack from the */ -static int recv_pkt(gitno_buffer *buf) +static int _git_uploadpack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - const char *ptr = buf->data, *line_end; - git_pkt *pkt; - int pkt_type, error; - - do { - /* Wait for max. 1 second */ - if ((error = gitno_select_in(buf, 1, 0)) < 0) { - return -1; - } else if (error == 0) { - /* - * Some servers don't respond immediately, so if this - * happens, we keep sending information until it - * answers. Pretend we received a NAK to convince higher - * layers to do so. - */ - return GIT_PKT_NAK; - } - - if ((error = gitno_recv(buf)) < 0) - return -1; - - error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); - if (error == GIT_EBUFS) - continue; - if (error < 0) - return -1; - } while (error); - - gitno_consume(buf, line_end); - pkt_type = pkt->type; - git__free(pkt); - - return pkt_type; + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call UPLOADPACK_LS before UPLOADPACK"); + return -1; } -static int git_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int _git_receivepack_ls( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - git_revwalk *walk; - git_oid oid; - int error; - unsigned int i; - git_buf data = GIT_BUF_INIT; - gitno_buffer *buf = &t->buf; + char *host, *port, *user=NULL, *pass=NULL; + git_stream *s; - if (git_pkt_buffer_wants(wants, &t->caps, &data) < 0) - return -1; + *stream = NULL; - if (git_fetch_setup_walk(&walk, repo) < 0) - goto on_error; + if (!git__prefixcmp(url, prefix_git)) + url += strlen(prefix_git); - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) - goto on_error; + if (git_stream_alloc(t, url, cmd_receivepack, stream) < 0) + return -1; - git_buf_clear(&data); - /* - * We don't support any kind of ACK extensions, so the negotiation - * boils down to sending what we have and listening for an ACK - * every once in a while. - */ - i = 0; - while ((error = git_revwalk_next(&oid, walk)) == 0) { - git_pkt_buffer_have(&oid, &data); - i++; - if (i % 20 == 0) { - int pkt_type; - - git_pkt_buffer_flush(&data); - if (git_buf_oom(&data)) - goto on_error; - - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) - goto on_error; - - pkt_type = recv_pkt(buf); - - if (pkt_type == GIT_PKT_ACK) { - break; - } else if (pkt_type == GIT_PKT_NAK) { - continue; - } else { - giterr_set(GITERR_NET, "Unexpected pkt type"); - goto on_error; - } - - } - } - if (error < 0 && error != GIT_REVWALKOVER) + s = (git_stream *)*stream; + + if (gitno_extract_url_parts(&host, &port, &user, &pass, url, GIT_DEFAULT_PORT) < 0) goto on_error; - /* Tell the other end that we're done negotiating */ - git_buf_clear(&data); - git_pkt_buffer_flush(&data); - git_pkt_buffer_done(&data); - if (gitno_send(t->socket, data.ptr, data.size, 0) < 0) + if (gitno_connect(&s->socket, host, port, 0) < 0) goto on_error; - git_buf_free(&data); - git_revwalk_free(walk); + t->current_stream = s; + git__free(host); + git__free(port); + git__free(user); + git__free(pass); return 0; on_error: - git_buf_free(&data); - git_revwalk_free(walk); + if (*stream) + git_stream_free(*stream); + + git__free(host); + git__free(port); return -1; } -static int git_download_pack(git_transport *transport, git_repository *repo, git_off_t *bytes, git_indexer_stats *stats) +static int _git_receivepack( + git_subtransport *t, + const char *url, + git_smart_subtransport_stream **stream) { - transport_git *t = (transport_git *) transport; - int error = 0, read_bytes; - gitno_buffer *buf = &t->buf; - git_pkt *pkt; - const char *line_end, *ptr; - - /* - * For now, we ignore everything and wait for the pack - */ - do { - ptr = buf->data; - /* Whilst we're searching for the pack */ - while (1) { - if (buf->offset == 0) { - break; - } - - error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); - if (error == GIT_EBUFS) - break; - - if (error < 0) - return error; - - if (pkt->type == GIT_PKT_PACK) { - git__free(pkt); - return git_fetch__download_pack(buf->data, buf->offset, t->socket, repo, bytes, stats); - } - - /* For now we don't care about anything */ - git__free(pkt); - gitno_consume(buf, line_end); - } - - read_bytes = gitno_recv(buf); - } while (read_bytes); - - return read_bytes; + GIT_UNUSED(url); + + if (t->current_stream) { + *stream = &t->current_stream->parent; + return 0; + } + + giterr_set(GITERR_NET, "Must call RECEIVEPACK_LS before RECEIVEPACK"); + return -1; } -static int git_close(git_transport *transport) +static int _git_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) { - transport_git *t = (transport_git*) transport; + git_subtransport *t = (git_subtransport *) subtransport; - /* Can't do anything if there's an error, so don't bother checking */ - git_pkt_send_flush(t->socket); - if (gitno_close(t->socket) < 0) { - giterr_set(GITERR_NET, "Failed to close socket"); - return -1; + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + return _git_uploadpack_ls(t, url, stream); + + case GIT_SERVICE_UPLOADPACK: + return _git_uploadpack(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK_LS: + return _git_receivepack_ls(t, url, stream); + + case GIT_SERVICE_RECEIVEPACK: + return _git_receivepack(t, url, stream); } -#ifdef GIT_WIN32 - WSACleanup(); -#endif + *stream = NULL; + return -1; +} + +static int _git_close(git_smart_subtransport *subtransport) +{ + git_subtransport *t = (git_subtransport *) subtransport; + + assert(!t->current_stream); + + GIT_UNUSED(t); return 0; } -static void git_free(git_transport *transport) +static void _git_free(git_smart_subtransport *subtransport) { - transport_git *t = (transport_git *) transport; - git_vector *refs = &t->refs; - unsigned int i; + git_subtransport *t = (git_subtransport *) subtransport; - for (i = 0; i < refs->length; ++i) { - git_pkt *p = git_vector_get(refs, i); - git_pkt_free(p); - } + assert(!t->current_stream); - git_vector_free(refs); - git__free(t->heads); - git_buf_free(&t->proto.buf); - git__free(t->parent.url); git__free(t); } -int git_transport_git(git_transport **out) +int git_smart_subtransport_git(git_smart_subtransport **out, git_transport *owner) { - transport_git *t; -#ifdef GIT_WIN32 - int ret; -#endif - - t = git__malloc(sizeof(transport_git)); - GITERR_CHECK_ALLOC(t); - - memset(t, 0x0, sizeof(transport_git)); + git_subtransport *t; - t->parent.connect = git_connect; - t->parent.ls = git_ls; - t->parent.negotiate_fetch = git_negotiate_fetch; - t->parent.download_pack = git_download_pack; - t->parent.close = git_close; - t->parent.free = git_free; - t->proto.refs = &t->refs; - t->proto.transport = (git_transport *) t; + if (!out) + return -1; - *out = (git_transport *) t; + t = git__calloc(sizeof(git_subtransport), 1); + GITERR_CHECK_ALLOC(t); -#ifdef GIT_WIN32 - ret = WSAStartup(MAKEWORD(2,2), &t->wsd); - if (ret != 0) { - git_free(*out); - giterr_set(GITERR_NET, "Winsock init failed"); - return -1; - } -#endif + t->owner = owner; + t->parent.action = _git_action; + t->parent.close = _git_close; + t->parent.free = _git_free; + *out = (git_smart_subtransport *) t; return 0; } diff --git a/src/transports/http.c b/src/transports/http.c index 2a8ebbb09..eca06ead2 100644 --- a/src/transports/http.c +++ b/src/transports/http.c @@ -1,25 +1,35 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * 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. */ +#ifndef GIT_WINHTTP -#include <stdlib.h> #include "git2.h" #include "http_parser.h" - -#include "transport.h" -#include "common.h" -#include "netops.h" #include "buffer.h" -#include "pkt.h" -#include "refs.h" -#include "pack.h" -#include "fetch.h" -#include "filebuf.h" -#include "repository.h" -#include "protocol.h" +#include "netops.h" +#include "smart.h" + +static const char *prefix_http = "http://"; +static const char *prefix_https = "https://"; +static const char *upload_pack_service = "upload-pack"; +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_service = "receive-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; +static const char *get_verb = "GET"; +static const char *post_verb = "POST"; +static const char *basic_authtype = "Basic"; + +#define OWNING_SUBTRANSPORT(s) ((http_subtransport *)(s)->parent.subtransport) + +#define PARSE_ERROR_GENERIC -1 +#define PARSE_ERROR_REPLAY -2 + +#define CHUNK_SIZE 4096 enum last_cb { NONE, @@ -27,54 +37,132 @@ enum last_cb { VALUE }; +typedef enum { + GIT_HTTP_AUTH_BASIC = 1, +} http_authmechanism_t; + typedef struct { - git_transport parent; - git_protocol proto; - git_vector refs; - git_vector common; - GIT_SOCKET socket; - git_buf buf; - git_remote_head **heads; - int error; - int transfer_finished :1, - ct_found :1, - ct_finished :1, - pack_ready :1; - enum last_cb last_cb; - http_parser parser; - char *content_type; + git_smart_subtransport_stream parent; + const char *service; + const char *service_url; + char *redirect_url; + const char *verb; + char *chunk_buffer; + unsigned chunk_buffer_len; + unsigned sent_request : 1, + received_response : 1, + chunked : 1, + redirect_count : 3; +} http_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + gitno_socket socket; + const char *path; char *host; char *port; - char *service; - git_transport_caps caps; -#ifdef GIT_WIN32 - WSADATA wsd; -#endif -} transport_http; - -static int gen_request(git_buf *buf, const char *url, const char *host, const char *op, - const char *service, ssize_t content_length, int ls) + char *user_from_url; + char *pass_from_url; + git_cred *cred; + git_cred *url_cred; + http_authmechanism_t auth_mechanism; + unsigned connected : 1, + use_ssl : 1; + + /* Parser structures */ + http_parser parser; + http_parser_settings settings; + gitno_buffer parse_buffer; + git_buf parse_header_name; + git_buf parse_header_value; + char parse_buffer_data[2048]; + char *content_type; + char *location; + git_vector www_authenticate; + enum last_cb last_cb; + int parse_error; + unsigned parse_finished : 1; +} http_subtransport; + +typedef struct { + http_stream *s; + http_subtransport *t; + + /* Target buffer details from read() */ + char *buffer; + size_t buf_size; + size_t *bytes_read; +} parser_context; + +static int apply_basic_credential(git_buf *buf, git_cred *cred) { - const char *path = url; + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + git_buf raw = GIT_BUF_INIT; + int error = -1; - path = strchr(path, '/'); - if (path == NULL) /* Is 'git fetch http://host.com/' valid? */ - path = "/"; + git_buf_printf(&raw, "%s:%s", c->username, c->password); + + if (git_buf_oom(&raw) || + git_buf_puts(buf, "Authorization: Basic ") < 0 || + git_buf_put_base64(buf, git_buf_cstr(&raw), raw.size) < 0 || + git_buf_puts(buf, "\r\n") < 0) + goto on_error; + + error = 0; + +on_error: + if (raw.size) + memset(raw.ptr, 0x0, raw.size); + + git_buf_free(&raw); + return error; +} + +static int gen_request( + git_buf *buf, + http_stream *s, + size_t content_length) +{ + http_subtransport *t = OWNING_SUBTRANSPORT(s); + + if (!t->path) + t->path = "/"; + + /* If we were redirected, make sure to respect that here */ + if (s->redirect_url) + git_buf_printf(buf, "%s %s HTTP/1.1\r\n", s->verb, s->redirect_url); + else + git_buf_printf(buf, "%s %s%s HTTP/1.1\r\n", s->verb, t->path, s->service_url); - if (ls) { - git_buf_printf(buf, "%s %s/info/refs?service=git-%s HTTP/1.1\r\n", op, path, service); - } else { - git_buf_printf(buf, "%s %s/git-%s HTTP/1.1\r\n", op, path, service); - } git_buf_puts(buf, "User-Agent: git/1.0 (libgit2 " LIBGIT2_VERSION ")\r\n"); - git_buf_printf(buf, "Host: %s\r\n", host); - if (content_length > 0) { - git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", service); - git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", service); - git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length); - } else { + git_buf_printf(buf, "Host: %s\r\n", t->host); + + if (s->chunked || content_length > 0) { + git_buf_printf(buf, "Accept: application/x-git-%s-result\r\n", s->service); + git_buf_printf(buf, "Content-Type: application/x-git-%s-request\r\n", s->service); + + if (s->chunked) + git_buf_puts(buf, "Transfer-Encoding: chunked\r\n"); + else + git_buf_printf(buf, "Content-Length: %"PRIuZ "\r\n", content_length); + } else git_buf_puts(buf, "Accept: */*\r\n"); + + /* Apply credentials to the request */ + if (t->cred && t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && + t->auth_mechanism == GIT_HTTP_AUTH_BASIC && + apply_basic_credential(buf, t->cred) < 0) + return -1; + + /* Use url-parsed basic auth if username and password are both provided */ + if (!t->cred && t->user_from_url && t->pass_from_url) { + if (!t->url_cred && + git_cred_userpass_plaintext_new(&t->url_cred, t->user_from_url, t->pass_from_url) < 0) + return -1; + if (apply_basic_credential(buf, t->url_cred) < 0) return -1; } + git_buf_puts(buf, "\r\n"); if (git_buf_oom(buf)) @@ -83,600 +171,783 @@ static int gen_request(git_buf *buf, const char *url, const char *host, const ch return 0; } -static int do_connect(transport_http *t, const char *host, const char *port) +static int parse_unauthorized_response( + git_vector *www_authenticate, + int *allowed_types, + http_authmechanism_t *auth_mechanism) { - GIT_SOCKET s; + unsigned i; + char *entry; + + git_vector_foreach(www_authenticate, i, entry) { + if (!strncmp(entry, basic_authtype, 5) && + (entry[5] == '\0' || entry[5] == ' ')) { + *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + *auth_mechanism = GIT_HTTP_AUTH_BASIC; + } + } - if (t->parent.connected && http_should_keep_alive(&t->parser)) - return 0; + return 0; +} - if (gitno_connect(&s, host, port) < 0) - return -1; +static int on_header_ready(http_subtransport *t) +{ + git_buf *name = &t->parse_header_name; + git_buf *value = &t->parse_header_value; + + if (!strcasecmp("Content-Type", git_buf_cstr(name))) { + if (!t->content_type) { + t->content_type = git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(t->content_type); + } + } + else if (!strcmp("WWW-Authenticate", git_buf_cstr(name))) { + char *dup = git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(dup); - t->socket = s; - t->parent.connected = 1; + git_vector_insert(&t->www_authenticate, dup); + } + else if (!strcasecmp("Location", git_buf_cstr(name))) { + if (!t->location) { + t->location= git__strdup(git_buf_cstr(value)); + GITERR_CHECK_ALLOC(t->location); + } + } return 0; } -/* - * The HTTP parser is streaming, so we need to wait until we're in the - * field handler before we can be sure that we can store the previous - * value. Right now, we only care about the - * Content-Type. on_header_{field,value} should be kept generic enough - * to work for any request. - */ - -static const char *typestr = "Content-Type"; - static int on_header_field(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; - - if (t->last_cb == VALUE && t->ct_found) { - t->ct_finished = 1; - t->ct_found = 0; - t->content_type = git__strdup(git_buf_cstr(buf)); - GITERR_CHECK_ALLOC(t->content_type); - git_buf_clear(buf); - } + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (t->ct_found) { - t->last_cb = FIELD; - return 0; - } + /* Both parse_header_name and parse_header_value are populated + * and ready for consumption */ + if (VALUE == t->last_cb) + if (on_header_ready(t) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; - if (t->last_cb != FIELD) - git_buf_clear(buf); + if (NONE == t->last_cb || VALUE == t->last_cb) + git_buf_clear(&t->parse_header_name); - git_buf_put(buf, str, len); - t->last_cb = FIELD; + if (git_buf_put(&t->parse_header_name, str, len) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; - return git_buf_oom(buf); + t->last_cb = FIELD; + return 0; } static int on_header_value(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (t->ct_finished) { - t->last_cb = VALUE; - return 0; - } + assert(NONE != t->last_cb); - if (t->last_cb == VALUE) - git_buf_put(buf, str, len); + if (FIELD == t->last_cb) + git_buf_clear(&t->parse_header_value); - if (t->last_cb == FIELD && !strcmp(git_buf_cstr(buf), typestr)) { - t->ct_found = 1; - git_buf_clear(buf); - git_buf_put(buf, str, len); - } + if (git_buf_put(&t->parse_header_value, str, len) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; t->last_cb = VALUE; - - return git_buf_oom(buf); + return 0; } static int on_headers_complete(http_parser *parser) { - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; + http_stream *s = ctx->s; + git_buf buf = GIT_BUF_INIT; + + /* Both parse_header_name and parse_header_value are populated + * and ready for consumption. */ + if (VALUE == t->last_cb) + if (on_header_ready(t) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + /* Check for an authentication failure. */ + if (parser->status_code == 401 && + get_verb == s->verb && + t->owner->cred_acquire_cb) { + int allowed_types = 0; + + if (parse_unauthorized_response(&t->www_authenticate, + &allowed_types, &t->auth_mechanism) < 0) + return t->parse_error = PARSE_ERROR_GENERIC; + + if (allowed_types && + (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + + if (t->owner->cred_acquire_cb(&t->cred, + t->owner->url, + t->user_from_url, + allowed_types, + t->owner->cred_acquire_payload) < 0) + return PARSE_ERROR_GENERIC; + + assert(t->cred); + + /* Successfully acquired a credential. */ + return t->parse_error = PARSE_ERROR_REPLAY; + } + } - /* The content-type is text/plain for 404, so don't validate */ - if (parser->status_code == 404) { - git_buf_clear(buf); - return 0; + /* Check for a redirect. + * Right now we only permit a redirect to the same hostname. */ + if ((parser->status_code == 301 || + parser->status_code == 302 || + (parser->status_code == 303 && get_verb == s->verb) || + parser->status_code == 307) && + t->location) { + + if (s->redirect_count >= 7) { + giterr_set(GITERR_NET, "Too many redirects"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + if (t->location[0] != '/') { + giterr_set(GITERR_NET, "Only relative redirects are supported"); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + /* Set the redirect URL on the stream. This is a transfer of + * ownership of the memory. */ + if (s->redirect_url) + git__free(s->redirect_url); + + s->redirect_url = t->location; + t->location = NULL; + + t->connected = 0; + s->redirect_count++; + + return t->parse_error = PARSE_ERROR_REPLAY; } - if (t->content_type == NULL) { - t->content_type = git__strdup(git_buf_cstr(buf)); - if (t->content_type == NULL) - return t->error = -1; + /* Check for a 200 HTTP status code. */ + if (parser->status_code != 200) { + giterr_set(GITERR_NET, + "Unexpected HTTP status code: %d", + parser->status_code); + return t->parse_error = PARSE_ERROR_GENERIC; } - git_buf_clear(buf); - git_buf_printf(buf, "application/x-git-%s-advertisement", t->service); - if (git_buf_oom(buf)) - return t->error = -1; + /* The response must contain a Content-Type header. */ + if (!t->content_type) { + giterr_set(GITERR_NET, "No Content-Type header in response"); + return t->parse_error = PARSE_ERROR_GENERIC; + } - if (strcmp(t->content_type, git_buf_cstr(buf))) - return t->error = -1; + /* The Content-Type header must match our expectation. */ + if (get_verb == s->verb) + git_buf_printf(&buf, + "application/x-git-%s-advertisement", + ctx->s->service); + else + git_buf_printf(&buf, + "application/x-git-%s-result", + ctx->s->service); + + if (git_buf_oom(&buf)) + return t->parse_error = PARSE_ERROR_GENERIC; + + if (strcmp(t->content_type, git_buf_cstr(&buf))) { + git_buf_free(&buf); + giterr_set(GITERR_NET, + "Invalid Content-Type: %s", + t->content_type); + return t->parse_error = PARSE_ERROR_GENERIC; + } + + git_buf_free(&buf); - git_buf_clear(buf); return 0; } -static int on_body_store_refs(http_parser *parser, const char *str, size_t len) +static int on_message_complete(http_parser *parser) { - transport_http *t = (transport_http *) parser->data; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - if (parser->status_code == 404) { - return git_buf_put(&t->buf, str, len); - } + t->parse_finished = 1; - return git_protocol_store_refs(&t->proto, str, len); + return 0; } -static int on_message_complete(http_parser *parser) +static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) { - transport_http *t = (transport_http *) parser->data; + parser_context *ctx = (parser_context *) parser->data; + http_subtransport *t = ctx->t; - t->transfer_finished = 1; - - if (parser->status_code == 404) { - giterr_set(GITERR_NET, "Remote error: %s", git_buf_cstr(&t->buf)); - t->error = -1; + if (ctx->buf_size < len) { + giterr_set(GITERR_NET, "Can't fit data in the buffer"); + return t->parse_error = PARSE_ERROR_GENERIC; } + memcpy(ctx->buffer, str, len); + *(ctx->bytes_read) += len; + ctx->buffer += len; + ctx->buf_size -= len; + return 0; } -static int store_refs(transport_http *t) +static void clear_parser_state(http_subtransport *t) { - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; - git_pkt *pkt; - int ret; + unsigned i; + char *entry; http_parser_init(&t->parser, HTTP_RESPONSE); - t->parser.data = t; - memset(&settings, 0x0, sizeof(http_parser_settings)); - settings.on_header_field = on_header_field; - settings.on_header_value = on_header_value; - settings.on_headers_complete = on_headers_complete; - settings.on_body = on_body_store_refs; - settings.on_message_complete = on_message_complete; + gitno_buffer_setup(&t->socket, + &t->parse_buffer, + t->parse_buffer_data, + sizeof(t->parse_buffer_data)); - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); + t->last_cb = NONE; + t->parse_error = 0; + t->parse_finished = 0; - while(1) { - size_t parsed; - - if ((ret = gitno_recv(&buf)) < 0) - return -1; + git_buf_free(&t->parse_header_name); + git_buf_init(&t->parse_header_name, 0); - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - /* Both should happen at the same time */ - if (parsed != buf.offset || t->error < 0) - return t->error; + git_buf_free(&t->parse_header_value); + git_buf_init(&t->parse_header_value, 0); - gitno_consume_n(&buf, parsed); + git__free(t->content_type); + t->content_type = NULL; - if (ret == 0 || t->transfer_finished) - return 0; - } + git__free(t->location); + t->location = NULL; - pkt = git_vector_get(&t->refs, 0); - if (pkt == NULL || pkt->type != GIT_PKT_COMMENT) { - giterr_set(GITERR_NET, "Invalid HTTP response"); - return t->error = -1; - } else { - git_vector_remove(&t->refs, 0); - } + git_vector_foreach(&t->www_authenticate, i, entry) + git__free(entry); - return 0; + git_vector_free(&t->www_authenticate); } -static int http_connect(git_transport *transport, int direction) +static int write_chunk(gitno_socket *socket, const char *buffer, size_t len) { - transport_http *t = (transport_http *) transport; - int ret; - git_buf request = GIT_BUF_INIT; - const char *service = "upload-pack"; - const char *url = t->parent.url, *prefix = "http://"; + git_buf buf = GIT_BUF_INIT; + + /* Chunk header */ + git_buf_printf(&buf, "%X\r\n", (unsigned)len); + + if (git_buf_oom(&buf)) + return -1; - if (direction == GIT_DIR_PUSH) { - giterr_set(GITERR_NET, "Pushing over HTTP is not implemented"); + if (gitno_send(socket, buf.ptr, buf.size, 0) < 0) { + git_buf_free(&buf); return -1; } - t->parent.direction = direction; - if (git_vector_init(&t->refs, 16, NULL) < 0) + git_buf_free(&buf); + + /* Chunk body */ + if (len > 0 && gitno_send(socket, buffer, len, 0) < 0) + return -1; + + /* Chunk footer */ + if (gitno_send(socket, "\r\n", 2, 0) < 0) return -1; - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); + return 0; +} - if ((ret = gitno_extract_host_and_port(&t->host, &t->port, url, "80")) < 0) - goto cleanup; +static int http_connect(http_subtransport *t) +{ + int flags = 0; - t->service = git__strdup(service); - GITERR_CHECK_ALLOC(t->service); + if (t->connected && + http_should_keep_alive(&t->parser) && + http_body_is_final(&t->parser)) + return 0; - if ((ret = do_connect(t, t->host, t->port)) < 0) - goto cleanup; + if (t->socket.socket) + gitno_close(&t->socket); - /* Generate and send the HTTP request */ - if ((ret = gen_request(&request, url, t->host, "GET", service, 0, 1)) < 0) { - giterr_set(GITERR_NET, "Failed to generate request"); - goto cleanup; - } + if (t->use_ssl) { + int tflags; - if ((ret = gitno_send(t->socket, request.ptr, request.size, 0)) < 0) - goto cleanup; + if (t->owner->parent.read_flags(&t->owner->parent, &tflags) < 0) + return -1; - ret = store_refs(t); + flags |= GITNO_CONNECT_SSL; -cleanup: - git_buf_free(&request); - git_buf_clear(&t->buf); + if (GIT_TRANSPORTFLAGS_NO_CHECK_CERT & tflags) + flags |= GITNO_CONNECT_SSL_NO_CHECK_CERT; + } + + if (gitno_connect(&t->socket, t->host, t->port, flags) < 0) + return -1; - return ret; + t->connected = 1; + return 0; } -static int http_ls(git_transport *transport, git_headlist_cb list_cb, void *opaque) +static int http_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) { - transport_http *t = (transport_http *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_pkt_ref *p; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); + parser_context ctx; + size_t bytes_parsed; + +replay: + *bytes_read = 0; + + assert(t->connected); + + if (!s->sent_request) { + git_buf request = GIT_BUF_INIT; + + clear_parser_state(t); - git_vector_foreach(refs, i, p) { - if (p->type != GIT_PKT_REF) - continue; + if (gen_request(&request, s, 0) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); + return -1; + } - if (list_cb(&p->head, opaque) < 0) { - giterr_set(GITERR_NET, "The user callback returned error"); + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { + git_buf_free(&request); return -1; } + + git_buf_free(&request); + + s->sent_request = 1; } - return 0; -} + if (!s->received_response) { + if (s->chunked) { + assert(s->verb == post_verb); -static int on_body_parse_response(http_parser *parser, const char *str, size_t len) -{ - transport_http *t = (transport_http *) parser->data; - git_buf *buf = &t->buf; - git_vector *common = &t->common; - int error; - const char *line_end, *ptr; - - if (len == 0) { /* EOF */ - if (git_buf_len(buf) != 0) { - giterr_set(GITERR_NET, "Unexpected EOF"); - return t->error = -1; - } else { - return 0; + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0 && + write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Write the final chunk. */ + if (gitno_send(&t->socket, "0\r\n\r\n", 5, 0) < 0) + return -1; } + + s->received_response = 1; } - git_buf_put(buf, str, len); - ptr = buf->ptr; - while (1) { - git_pkt *pkt; + while (!*bytes_read && !t->parse_finished) { + t->parse_buffer.offset = 0; - if (git_buf_len(buf) == 0) - return 0; + if (gitno_recv(&t->parse_buffer) < 0) + return -1; - error = git_pkt_parse_line(&pkt, ptr, &line_end, git_buf_len(buf)); - if (error == GIT_EBUFS) { - return 0; /* Ask for more */ - } - if (error < 0) - return t->error = -1; + /* This call to http_parser_execute will result in invocations of the + * on_* family of callbacks. The most interesting of these is + * on_body_fill_buffer, which is called when data is ready to be copied + * into the target buffer. We need to marshal the buffer, buf_size, and + * bytes_read parameters to this callback. */ + ctx.t = t; + ctx.s = s; + ctx.buffer = buffer; + ctx.buf_size = buf_size; + ctx.bytes_read = bytes_read; - git_buf_consume(buf, line_end); + /* Set the context, call the parser, then unset the context. */ + t->parser.data = &ctx; - if (pkt->type == GIT_PKT_PACK) { - git__free(pkt); - t->pack_ready = 1; - return 0; - } + bytes_parsed = http_parser_execute(&t->parser, + &t->settings, + t->parse_buffer.data, + t->parse_buffer.offset); - if (pkt->type == GIT_PKT_NAK) { - git__free(pkt); - return 0; - } + t->parser.data = NULL; + + /* If there was a handled authentication failure, then parse_error + * will have signaled us that we should replay the request. */ + if (PARSE_ERROR_REPLAY == t->parse_error) { + s->sent_request = 0; - if (pkt->type != GIT_PKT_ACK) { - git__free(pkt); - continue; + if (http_connect(t) < 0) + return -1; + + goto replay; } - if (git_vector_insert(common, pkt) < 0) + if (t->parse_error < 0) return -1; - } - return error; + if (bytes_parsed != t->parse_buffer.offset) { + giterr_set(GITERR_NET, + "HTTP parser error: %s", + http_errno_description((enum http_errno)t->parser.http_errno)); + return -1; + } + } + return 0; } -static int parse_response(transport_http *t) +static int http_stream_write_chunked( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - int ret = 0; - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); - http_parser_init(&t->parser, HTTP_RESPONSE); - t->parser.data = t; - t->transfer_finished = 0; - memset(&settings, 0x0, sizeof(http_parser_settings)); - settings.on_header_field = on_header_field; - settings.on_header_value = on_header_value; - settings.on_headers_complete = on_headers_complete; - settings.on_body = on_body_parse_response; - settings.on_message_complete = on_message_complete; + assert(t->connected); - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); + /* Send the request, if necessary */ + if (!s->sent_request) { + git_buf request = GIT_BUF_INIT; - while(1) { - size_t parsed; + clear_parser_state(t); - if ((ret = gitno_recv(&buf)) < 0) + if (gen_request(&request, s, 0) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); return -1; + } - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - /* Both should happen at the same time */ - if (parsed != buf.offset || t->error < 0) - return t->error; + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) { + git_buf_free(&request); + return -1; + } + + git_buf_free(&request); + + s->sent_request = 1; + } + + if (len > CHUNK_SIZE) { + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0) { + if (write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + } - gitno_consume_n(&buf, parsed); + /* Write chunk directly */ + if (write_chunk(&t->socket, buffer, len) < 0) + return -1; + } + else { + /* Append as much to the buffer as we can */ + int count = min(CHUNK_SIZE - s->chunk_buffer_len, len); + + if (!s->chunk_buffer) + s->chunk_buffer = git__malloc(CHUNK_SIZE); + + memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); + s->chunk_buffer_len += count; + buffer += count; + len -= count; - if (ret == 0 || t->transfer_finished || t->pack_ready) { - return 0; + /* Is the buffer full? If so, then flush */ + if (CHUNK_SIZE == s->chunk_buffer_len) { + if (write_chunk(&t->socket, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + if (len > 0) { + memcpy(s->chunk_buffer, buffer, len); + s->chunk_buffer_len = len; + } } } - return ret; + return 0; } -static int http_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int http_stream_write_single( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) { - transport_http *t = (transport_http *) transport; - int ret; - unsigned int i; - char buff[128]; - gitno_buffer buf; - git_revwalk *walk = NULL; - git_oid oid; - git_pkt_ack *pkt; - git_vector *common = &t->common; - const char *prefix = "http://", *url = t->parent.url; - git_buf request = GIT_BUF_INIT, data = GIT_BUF_INIT; - gitno_buffer_setup(&buf, buff, sizeof(buff), t->socket); - - /* TODO: Store url in the transport */ - if (!git__prefixcmp(url, prefix)) - url += strlen(prefix); - - if (git_vector_init(common, 16, NULL) < 0) - return -1; + http_stream *s = (http_stream *)stream; + http_subtransport *t = OWNING_SUBTRANSPORT(s); + git_buf request = GIT_BUF_INIT; - if (git_fetch_setup_walk(&walk, repo) < 0) + assert(t->connected); + + if (s->sent_request) { + giterr_set(GITERR_NET, "Subtransport configured for only one write"); return -1; + } - do { - if ((ret = do_connect(t, t->host, t->port)) < 0) - goto cleanup; + clear_parser_state(t); - if ((ret = git_pkt_buffer_wants(wants, &t->caps, &data)) < 0) - goto cleanup; + if (gen_request(&request, s, len) < 0) { + giterr_set(GITERR_NET, "Failed to generate request"); + return -1; + } - /* We need to send these on each connection */ - git_vector_foreach (common, i, pkt) { - if ((ret = git_pkt_buffer_have(&pkt->oid, &data)) < 0) - goto cleanup; - } + if (gitno_send(&t->socket, request.ptr, request.size, 0) < 0) + goto on_error; - i = 0; - while ((i < 20) && ((ret = git_revwalk_next(&oid, walk)) == 0)) { - if ((ret = git_pkt_buffer_have(&oid, &data)) < 0) - goto cleanup; + if (len && gitno_send(&t->socket, buffer, len, 0) < 0) + goto on_error; - i++; - } + git_buf_free(&request); + s->sent_request = 1; - git_pkt_buffer_done(&data); + return 0; + +on_error: + git_buf_free(&request); + return -1; +} - if ((ret = gen_request(&request, url, t->host, "POST", "upload-pack", data.size, 0)) < 0) - goto cleanup; +static void http_stream_free(git_smart_subtransport_stream *stream) +{ + http_stream *s = (http_stream *)stream; - if ((ret = gitno_send(t->socket, request.ptr, request.size, 0)) < 0) - goto cleanup; + if (s->chunk_buffer) + git__free(s->chunk_buffer); - if ((ret = gitno_send(t->socket, data.ptr, data.size, 0)) < 0) - goto cleanup; + if (s->redirect_url) + git__free(s->redirect_url); - git_buf_clear(&request); - git_buf_clear(&data); + git__free(s); +} - if (ret < 0 || i >= 256) - break; +static int http_stream_alloc(http_subtransport *t, + git_smart_subtransport_stream **stream) +{ + http_stream *s; - if ((ret = parse_response(t)) < 0) - goto cleanup; + if (!stream) + return -1; - if (t->pack_ready) { - ret = 0; - goto cleanup; - } + s = git__calloc(sizeof(http_stream), 1); + GITERR_CHECK_ALLOC(s); - } while(1); + s->parent.subtransport = &t->parent; + s->parent.read = http_stream_read; + s->parent.write = http_stream_write_single; + s->parent.free = http_stream_free; -cleanup: - git_buf_free(&request); - git_buf_free(&data); - git_revwalk_free(walk); - return ret; + *stream = (git_smart_subtransport_stream *)s; + return 0; } -typedef struct { - git_indexer_stream *idx; - git_indexer_stats *stats; - transport_http *transport; -} download_pack_cbdata; - -static int on_message_complete_download_pack(http_parser *parser) +static int http_uploadpack_ls( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - download_pack_cbdata *data = (download_pack_cbdata *) parser->data; + http_stream *s; + + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; - data->transport->transfer_finished = 1; + s->service = upload_pack_service; + s->service_url = upload_pack_ls_service_url; + s->verb = get_verb; return 0; } -static int on_body_download_pack(http_parser *parser, const char *str, size_t len) + +static int http_uploadpack( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - download_pack_cbdata *data = (download_pack_cbdata *) parser->data; - transport_http *t = data->transport; - git_indexer_stream *idx = data->idx; - git_indexer_stats *stats = data->stats; + http_stream *s; - return t->error = git_indexer_stream_add(idx, str, len, stats); + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; + + s->service = upload_pack_service; + s->service_url = upload_pack_service_url; + s->verb = post_verb; + + return 0; } -/* - * As the server is probably using Transfer-Encoding: chunked, we have - * to use the HTTP parser to download the pack instead of giving it to - * the simple downloader. Furthermore, we're using keep-alive - * connections, so the simple downloader would just hang. - */ -static int http_download_pack(git_transport *transport, git_repository *repo, git_off_t *bytes, git_indexer_stats *stats) +static int http_receivepack_ls( + http_subtransport *t, + git_smart_subtransport_stream **stream) { - transport_http *t = (transport_http *) transport; - git_buf *oldbuf = &t->buf; - int recvd; - http_parser_settings settings; - char buffer[1024]; - gitno_buffer buf; - git_indexer_stream *idx = NULL; - download_pack_cbdata data; + http_stream *s; + + if (http_stream_alloc(t, stream) < 0) + return -1; + + s = (http_stream *)*stream; + + s->service = receive_pack_service; + s->service_url = receive_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} - gitno_buffer_setup(&buf, buffer, sizeof(buffer), t->socket); +static int http_receivepack( + http_subtransport *t, + git_smart_subtransport_stream **stream) +{ + http_stream *s; - if (memcmp(oldbuf->ptr, "PACK", strlen("PACK"))) { - giterr_set(GITERR_NET, "The pack doesn't start with a pack signature"); + if (http_stream_alloc(t, stream) < 0) return -1; - } - if (git_indexer_stream_new(&idx, git_repository_path(repo)) < 0) + s = (http_stream *)*stream; + + /* Use Transfer-Encoding: chunked for this request */ + s->chunked = 1; + s->parent.write = http_stream_write_chunked; + + s->service = receive_pack_service; + s->service_url = receive_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int http_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + http_subtransport *t = (http_subtransport *)subtransport; + const char *default_port = NULL; + int ret; + + if (!stream) return -1; + if (!t->host || !t->port || !t->path) { + if (!git__prefixcmp(url, prefix_http)) { + url = url + strlen(prefix_http); + default_port = "80"; + } - /* - * This is part of the previous response, so we don't want to - * re-init the parser, just set these two callbacks. - */ - memset(stats, 0, sizeof(git_indexer_stats)); - data.stats = stats; - data.idx = idx; - data.transport = t; - t->parser.data = &data; - t->transfer_finished = 0; - memset(&settings, 0x0, sizeof(settings)); - settings.on_message_complete = on_message_complete_download_pack; - settings.on_body = on_body_download_pack; - *bytes = git_buf_len(oldbuf); - - if (git_indexer_stream_add(idx, git_buf_cstr(oldbuf), git_buf_len(oldbuf), stats) < 0) - goto on_error; + if (!git__prefixcmp(url, prefix_https)) { + url += strlen(prefix_https); + default_port = "443"; + t->use_ssl = 1; + } - do { - size_t parsed; + if (!default_port) + return -1; - if ((recvd = gitno_recv(&buf)) < 0) - goto on_error; + if ((ret = gitno_extract_url_parts(&t->host, &t->port, + &t->user_from_url, &t->pass_from_url, url, default_port)) < 0) + return ret; - parsed = http_parser_execute(&t->parser, &settings, buf.data, buf.offset); - if (parsed != buf.offset || t->error < 0) - goto on_error; + t->path = strchr(url, '/'); + } - *bytes += recvd; - gitno_consume_n(&buf, parsed); - } while (recvd > 0 && !t->transfer_finished); + if (http_connect(t) < 0) + return -1; - if (git_indexer_stream_finalize(idx, stats) < 0) - goto on_error; + switch (action) + { + case GIT_SERVICE_UPLOADPACK_LS: + return http_uploadpack_ls(t, stream); - git_indexer_stream_free(idx); - return 0; + case GIT_SERVICE_UPLOADPACK: + return http_uploadpack(t, stream); -on_error: - git_indexer_stream_free(idx); + case GIT_SERVICE_RECEIVEPACK_LS: + return http_receivepack_ls(t, stream); + + case GIT_SERVICE_RECEIVEPACK: + return http_receivepack(t, stream); + } + + *stream = NULL; return -1; } -static int http_close(git_transport *transport) +static int http_close(git_smart_subtransport *subtransport) { - transport_http *t = (transport_http *) transport; + http_subtransport *t = (http_subtransport *) subtransport; - if (gitno_close(t->socket) < 0) { - giterr_set(GITERR_OS, "Failed to close the socket: %s", strerror(errno)); - return -1; + clear_parser_state(t); + + if (t->socket.socket) { + gitno_close(&t->socket); + memset(&t->socket, 0x0, sizeof(gitno_socket)); } - return 0; -} + if (t->cred) { + t->cred->free(t->cred); + t->cred = NULL; + } + if (t->url_cred) { + t->url_cred->free(t->url_cred); + t->url_cred = NULL; + } -static void http_free(git_transport *transport) -{ - transport_http *t = (transport_http *) transport; - git_vector *refs = &t->refs; - git_vector *common = &t->common; - unsigned int i; - git_pkt *p; - -#ifdef GIT_WIN32 - /* cleanup the WSA context. note that this context - * can be initialized more than once with WSAStartup(), - * and needs to be cleaned one time for each init call - */ - WSACleanup(); -#endif - - git_vector_foreach(refs, i, p) { - git_pkt_free(p); + if (t->host) { + git__free(t->host); + t->host = NULL; } - git_vector_free(refs); - git_vector_foreach(common, i, p) { - git_pkt_free(p); + + if (t->port) { + git__free(t->port); + t->port = NULL; } - git_vector_free(common); - git_buf_free(&t->buf); - git_buf_free(&t->proto.buf); - git__free(t->heads); - git__free(t->content_type); - git__free(t->host); - git__free(t->port); - git__free(t->service); - git__free(t->parent.url); + + if (t->user_from_url) { + git__free(t->user_from_url); + t->user_from_url = NULL; + } + + if (t->pass_from_url) { + git__free(t->pass_from_url); + t->pass_from_url = NULL; + } + + return 0; +} + +static void http_free(git_smart_subtransport *subtransport) +{ + http_subtransport *t = (http_subtransport *) subtransport; + + http_close(subtransport); + git__free(t); } -int git_transport_http(git_transport **out) +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner) { - transport_http *t; + http_subtransport *t; - t = git__malloc(sizeof(transport_http)); - GITERR_CHECK_ALLOC(t); + if (!out) + return -1; - memset(t, 0x0, sizeof(transport_http)); + t = git__calloc(sizeof(http_subtransport), 1); + GITERR_CHECK_ALLOC(t); - t->parent.connect = http_connect; - t->parent.ls = http_ls; - t->parent.negotiate_fetch = http_negotiate_fetch; - t->parent.download_pack = http_download_pack; + t->owner = (transport_smart *)owner; + t->parent.action = http_action; t->parent.close = http_close; t->parent.free = http_free; - t->proto.refs = &t->refs; - t->proto.transport = (git_transport *) t; - -#ifdef GIT_WIN32 - /* on win32, the WSA context needs to be initialized - * before any socket calls can be performed */ - if (WSAStartup(MAKEWORD(2,2), &t->wsd) != 0) { - http_free((git_transport *) t); - giterr_set(GITERR_OS, "Winsock init failed"); - return -1; - } -#endif - *out = (git_transport *) t; + t->settings.on_header_field = on_header_field; + t->settings.on_header_value = on_header_value; + t->settings.on_headers_complete = on_headers_complete; + t->settings.on_body = on_body_fill_buffer; + t->settings.on_message_complete = on_message_complete; + + *out = (git_smart_subtransport *) t; return 0; } + +#endif /* !GIT_WINHTTP */ diff --git a/src/transports/local.c b/src/transports/local.c index 000993e69..8af970eac 100644 --- a/src/transports/local.c +++ b/src/transports/local.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2009-2012 the libgit2 contributors + * 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. @@ -10,16 +10,34 @@ #include "git2/repository.h" #include "git2/object.h" #include "git2/tag.h" +#include "git2/transport.h" +#include "git2/revwalk.h" +#include "git2/odb_backend.h" +#include "git2/pack.h" +#include "git2/commit.h" +#include "git2/revparse.h" +#include "git2/push.h" +#include "pack-objects.h" #include "refs.h" -#include "transport.h" #include "posix.h" #include "path.h" #include "buffer.h" +#include "repository.h" +#include "odb.h" +#include "push.h" +#include "remote.h" typedef struct { git_transport parent; + git_remote *owner; + char *url; + int direction; + int flags; + git_atomic cancelled; git_repository *repo; git_vector refs; + unsigned connected : 1, + have_refs : 1; } transport_local; static int add_ref(transport_local *t, const char *name) @@ -28,15 +46,28 @@ static int add_ref(transport_local *t, const char *name) git_remote_head *head; git_object *obj = NULL, *target = NULL; git_buf buf = GIT_BUF_INIT; + int error; - head = git__malloc(sizeof(git_remote_head)); + head = git__calloc(1, sizeof(git_remote_head)); GITERR_CHECK_ALLOC(head); head->name = git__strdup(name); GITERR_CHECK_ALLOC(head->name); - if (git_reference_name_to_oid(&head->oid, t->repo, name) < 0 || - git_vector_insert(&t->refs, head) < 0) + error = git_reference_name_to_id(&head->oid, t->repo, name); + if (error < 0) { + git__free(head->name); + git__free(head); + if (!strcmp(name, GIT_HEAD_FILE) && error == GIT_ENOTFOUND) { + /* This is actually okay. Empty repos often have a HEAD that points to + * a nonexistent "refs/heads/master". */ + giterr_clear(); + return 0; + } + return error; + } + + if (git_vector_insert(&t->refs, head) < 0) { git__free(head->name); git__free(head); @@ -52,14 +83,16 @@ static int add_ref(transport_local *t, const char *name) head = NULL; - /* If it's not an annotated tag, just get out */ - if (git_object_type(obj) != GIT_OBJ_TAG) { + /* If it's not an annotated tag, or if we're mocking + * git-receive-pack, just get out */ + if (git_object_type(obj) != GIT_OBJ_TAG || + t->direction != GIT_DIRECTION_FETCH) { git_object_free(obj); return 0; } /* And if it's a tag, peel it, and add it to the list */ - head = git__malloc(sizeof(git_remote_head)); + head = git__calloc(1, sizeof(git_remote_head)); GITERR_CHECK_ALLOC(head); if (git_buf_join(&buf, 0, name, peeled) < 0) return -1; @@ -92,14 +125,14 @@ static int store_refs(transport_local *t) assert(t); if (git_reference_list(&ref_names, t->repo, GIT_REF_LISTALL) < 0 || - git_vector_init(&t->refs, (unsigned int)ref_names.count, NULL) < 0) + git_vector_init(&t->refs, ref_names.count, NULL) < 0) goto on_error; /* Sort the references first */ git__tsort((void **)ref_names.strings, ref_names.count, &git__strcmp_cb); - /* Add HEAD */ - if (add_ref(t, GIT_HEAD_FILE) < 0) + /* Add HEAD iff direction is fetch */ + if (t->direction == GIT_DIRECTION_FETCH && add_ref(t, GIT_HEAD_FILE) < 0) goto on_error; for (i = 0; i < ref_names.count; ++i) { @@ -107,6 +140,7 @@ static int store_refs(transport_local *t) goto on_error; } + t->have_refs = 1; git_strarray_free(&ref_names); return 0; @@ -116,28 +150,16 @@ on_error: return -1; } -static int local_ls(git_transport *transport, git_headlist_cb list_cb, void *payload) -{ - transport_local *t = (transport_local *) transport; - git_vector *refs = &t->refs; - unsigned int i; - git_remote_head *h; - - assert(transport && transport->connected); - - git_vector_foreach(refs, i, h) { - if (list_cb(h, payload) < 0) - return -1; - } - - return 0; -} - /* * Try to open the url as a git directory. The direction doesn't * matter in this case because we're calulating the heads ourselves. */ -static int local_connect(git_transport *transport, int direction) +static int local_connect( + git_transport *transport, + const char *url, + git_cred_acquire_cb cred_acquire_cb, + void *cred_acquire_payload, + int direction, int flags) { git_repository *repo; int error; @@ -145,18 +167,24 @@ static int local_connect(git_transport *transport, int direction) const char *path; git_buf buf = GIT_BUF_INIT; - GIT_UNUSED(direction); + GIT_UNUSED(cred_acquire_cb); + GIT_UNUSED(cred_acquire_payload); + + t->url = git__strdup(url); + GITERR_CHECK_ALLOC(t->url); + t->direction = direction; + t->flags = flags; /* The repo layer doesn't want the prefix */ - if (!git__prefixcmp(transport->url, "file://")) { - if (git_path_fromurl(&buf, transport->url) < 0) { + if (!git__prefixcmp(t->url, "file://")) { + if (git_path_fromurl(&buf, t->url) < 0) { git_buf_free(&buf); return -1; } path = git_buf_cstr(&buf); } else { /* We assume transport->url is already a path */ - path = transport->url; + path = t->url; } error = git_repository_open(&repo, path); @@ -171,47 +199,411 @@ static int local_connect(git_transport *transport, int direction) if (store_refs(t) < 0) return -1; - t->parent.connected = 1; + t->connected = 1; + + return 0; +} + +static int local_ls(git_transport *transport, git_headlist_cb list_cb, void *payload) +{ + transport_local *t = (transport_local *)transport; + unsigned int i; + git_remote_head *head = NULL; + + if (!t->have_refs) { + giterr_set(GITERR_NET, "The transport has not yet loaded the refs"); + return -1; + } + + git_vector_foreach(&t->refs, i, head) { + if (list_cb(head, payload)) + return GIT_EUSER; + } return 0; } -static int local_negotiate_fetch(git_transport *transport, git_repository *repo, const git_vector *wants) +static int local_negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_remote_head * const *refs, + size_t count) { - GIT_UNUSED(transport); - GIT_UNUSED(repo); - GIT_UNUSED(wants); + transport_local *t = (transport_local*)transport; + git_remote_head *rhead; + unsigned int i; - giterr_set(GITERR_NET, "Fetch via local transport isn't implemented. Sorry"); - return -1; + GIT_UNUSED(refs); + GIT_UNUSED(count); + + /* Fill in the loids */ + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + + int error = git_revparse_single(&obj, repo, rhead->name); + if (!error) + git_oid_cpy(&rhead->loid, git_object_id(obj)); + else if (error != GIT_ENOTFOUND) + return error; + git_object_free(obj); + giterr_clear(); + } + + return 0; +} + +static int local_push_copy_object( + git_odb *local_odb, + git_odb *remote_odb, + git_pobject *obj) +{ + int error = 0; + git_odb_object *odb_obj = NULL; + git_odb_stream *odb_stream; + size_t odb_obj_size; + git_otype odb_obj_type; + git_oid remote_odb_obj_oid; + + /* Object already exists in the remote ODB; do nothing and return 0*/ + if (git_odb_exists(remote_odb, &obj->id)) + return 0; + + if ((error = git_odb_read(&odb_obj, local_odb, &obj->id)) < 0) + return error; + + odb_obj_size = git_odb_object_size(odb_obj); + odb_obj_type = git_odb_object_type(odb_obj); + + if ((error = git_odb_open_wstream(&odb_stream, remote_odb, + odb_obj_size, odb_obj_type)) < 0) + goto on_error; + + if (odb_stream->write(odb_stream, (char *)git_odb_object_data(odb_obj), + odb_obj_size) < 0 || + odb_stream->finalize_write(&remote_odb_obj_oid, odb_stream) < 0) { + error = -1; + } else if (git_oid_cmp(&obj->id, &remote_odb_obj_oid) != 0) { + giterr_set(GITERR_ODB, "Error when writing object to remote odb " + "during local push operation. Remote odb object oid does not " + "match local oid."); + error = -1; + } + + odb_stream->free(odb_stream); + +on_error: + git_odb_object_free(odb_obj); + return error; +} + +static int local_push_update_remote_ref( + git_repository *remote_repo, + const char *lref, + const char *rref, + git_oid *loid, + git_oid *roid) +{ + int error; + git_reference *remote_ref = NULL; + + /* rref will be NULL if it is implicit in the pushspec (e.g. 'b1:') */ + rref = rref ? rref : lref; + + if (lref) { + /* Create or update a ref */ + if ((error = git_reference_create(NULL, remote_repo, rref, loid, + !git_oid_iszero(roid))) < 0) + return error; + } else { + /* Delete a ref */ + if ((error = git_reference_lookup(&remote_ref, remote_repo, rref)) < 0) { + if (error == GIT_ENOTFOUND) + error = 0; + return error; + } + + if ((error = git_reference_delete(remote_ref)) < 0) + return error; + + git_reference_free(remote_ref); + } + + return 0; +} + +static int local_push( + git_transport *transport, + git_push *push) +{ + transport_local *t = (transport_local *)transport; + git_odb *remote_odb = NULL; + git_odb *local_odb = NULL; + git_repository *remote_repo = NULL; + push_spec *spec; + char *url = NULL; + int error; + unsigned int i; + size_t j; + + if ((error = git_repository_open(&remote_repo, push->remote->url)) < 0) + return error; + + /* We don't currently support pushing locally to non-bare repos. Proper + non-bare repo push support would require checking configs to see if + we should override the default 'don't let this happen' behavior */ + if (!remote_repo->is_bare) { + error = -1; + goto on_error; + } + + if ((error = git_repository_odb__weakptr(&remote_odb, remote_repo)) < 0 || + (error = git_repository_odb__weakptr(&local_odb, push->repo)) < 0) + goto on_error; + + for (i = 0; i < push->pb->nr_objects; i++) { + if ((error = local_push_copy_object(local_odb, remote_odb, + &push->pb->object_list[i])) < 0) + goto on_error; + } + + push->unpack_ok = 1; + + git_vector_foreach(&push->specs, j, spec) { + push_status *status; + const git_error *last; + char *ref = spec->rref ? spec->rref : spec->lref; + + status = git__calloc(sizeof(push_status), 1); + if (!status) + goto on_error; + + status->ref = git__strdup(ref); + if (!status->ref) { + git_push_status_free(status); + goto on_error; + } + + error = local_push_update_remote_ref(remote_repo, spec->lref, spec->rref, + &spec->loid, &spec->roid); + + switch (error) { + case GIT_OK: + break; + case GIT_EINVALIDSPEC: + status->msg = git__strdup("funny refname"); + break; + case GIT_ENOTFOUND: + status->msg = git__strdup("Remote branch not found to delete"); + break; + default: + last = giterr_last(); + + if (last && last->message) + status->msg = git__strdup(last->message); + else + status->msg = git__strdup("Unspecified error encountered"); + break; + } + + /* failed to allocate memory for a status message */ + if (error < 0 && !status->msg) { + git_push_status_free(status); + goto on_error; + } + + /* failed to insert the ref update status */ + if ((error = git_vector_insert(&push->status, status)) < 0) { + git_push_status_free(status); + goto on_error; + } + } + + if (push->specs.length) { + int flags = t->flags; + url = git__strdup(t->url); + + if (!url || t->parent.close(&t->parent) < 0 || + t->parent.connect(&t->parent, url, + push->remote->cred_acquire_cb, NULL, GIT_DIRECTION_PUSH, flags)) + goto on_error; + } + + error = 0; + +on_error: + git_repository_free(remote_repo); + git__free(url); + + return error; +} + +typedef struct foreach_data { + git_transfer_progress *stats; + git_transfer_progress_callback progress_cb; + void *progress_payload; + git_odb_writepack *writepack; +} foreach_data; + +static int foreach_cb(void *buf, size_t len, void *payload) +{ + foreach_data *data = (foreach_data*)payload; + + data->stats->received_bytes += len; + return data->writepack->add(data->writepack, buf, len, data->stats); +} + +static int local_download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload) +{ + transport_local *t = (transport_local*)transport; + git_revwalk *walk = NULL; + git_remote_head *rhead; + unsigned int i; + int error = -1; + git_oid oid; + git_packbuilder *pack = NULL; + git_odb_writepack *writepack = NULL; + git_odb *odb = NULL; + + if ((error = git_revwalk_new(&walk, t->repo)) < 0) + goto cleanup; + git_revwalk_sorting(walk, GIT_SORT_TIME); + + if ((error = git_packbuilder_new(&pack, t->repo)) < 0) + goto cleanup; + + stats->total_objects = 0; + stats->indexed_objects = 0; + stats->received_objects = 0; + stats->received_bytes = 0; + + git_vector_foreach(&t->refs, i, rhead) { + git_object *obj; + if ((error = git_object_lookup(&obj, t->repo, &rhead->oid, GIT_OBJ_ANY)) < 0) + goto cleanup; + + if (git_object_type(obj) == GIT_OBJ_COMMIT) { + /* Revwalker includes only wanted commits */ + error = git_revwalk_push(walk, &rhead->oid); + if (!git_oid_iszero(&rhead->loid)) + error = git_revwalk_hide(walk, &rhead->loid); + } else { + /* Tag or some other wanted object. Add it on its own */ + error = git_packbuilder_insert(pack, &rhead->oid, rhead->name); + } + git_object_free(obj); + } + + /* Walk the objects, building a packfile */ + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0) + goto cleanup; + + while ((error = git_revwalk_next(&oid, walk)) == 0) { + git_commit *commit; + + /* Skip commits we already have */ + if (git_odb_exists(odb, &oid)) continue; + + if (!git_object_lookup((git_object**)&commit, t->repo, &oid, GIT_OBJ_COMMIT)) { + const git_oid *tree_oid = git_commit_tree_id(commit); + + /* Add the commit and its tree */ + if ((error = git_packbuilder_insert(pack, &oid, NULL)) < 0 || + (error = git_packbuilder_insert_tree(pack, tree_oid)) < 0) { + git_commit_free(commit); + goto cleanup; + } + + git_commit_free(commit); + } + } + + if ((error = git_odb_write_pack(&writepack, odb, progress_cb, progress_payload)) < 0) + goto cleanup; + + /* Write the data to the ODB */ + { + foreach_data data = {0}; + data.stats = stats; + data.progress_cb = progress_cb; + data.progress_payload = progress_payload; + data.writepack = writepack; + + if ((error = git_packbuilder_foreach(pack, foreach_cb, &data)) < 0) + goto cleanup; + } + error = writepack->commit(writepack, stats); + +cleanup: + if (writepack) writepack->free(writepack); + git_packbuilder_free(pack); + git_revwalk_free(walk); + return error; +} + +static int local_is_connected(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + return t->connected; +} + +static int local_read_flags(git_transport *transport, int *flags) +{ + transport_local *t = (transport_local *)transport; + + *flags = t->flags; + + return 0; +} + +static void local_cancel(git_transport *transport) +{ + transport_local *t = (transport_local *)transport; + + git_atomic_set(&t->cancelled, 1); } static int local_close(git_transport *transport) { transport_local *t = (transport_local *)transport; - git_repository_free(t->repo); - t->repo = NULL; + t->connected = 0; + + if (t->repo) { + git_repository_free(t->repo); + t->repo = NULL; + } + + if (t->url) { + git__free(t->url); + t->url = NULL; + } return 0; } static void local_free(git_transport *transport) { - unsigned int i; - transport_local *t = (transport_local *) transport; - git_vector *vec = &t->refs; - git_remote_head *h; + transport_local *t = (transport_local *)transport; + size_t i; + git_remote_head *head; - assert(transport); + /* Close the transport, if it's still open. */ + local_close(transport); - git_vector_foreach (vec, i, h) { - git__free(h->name); - git__free(h); + git_vector_foreach(&t->refs, i, head) { + git__free(head->name); + git__free(head); } - git_vector_free(vec); - git__free(t->parent.url); + git_vector_free(&t->refs); + + /* Free the transport */ git__free(t); } @@ -219,20 +611,28 @@ static void local_free(git_transport *transport) * Public API * **************/ -int git_transport_local(git_transport **out) +int git_transport_local(git_transport **out, git_remote *owner, void *param) { transport_local *t; - t = git__malloc(sizeof(transport_local)); - GITERR_CHECK_ALLOC(t); + GIT_UNUSED(param); - memset(t, 0x0, sizeof(transport_local)); + t = git__calloc(1, sizeof(transport_local)); + GITERR_CHECK_ALLOC(t); + t->parent.version = GIT_TRANSPORT_VERSION; t->parent.connect = local_connect; - t->parent.ls = local_ls; t->parent.negotiate_fetch = local_negotiate_fetch; + t->parent.download_pack = local_download_pack; + t->parent.push = local_push; t->parent.close = local_close; t->parent.free = local_free; + t->parent.ls = local_ls; + t->parent.is_connected = local_is_connected; + t->parent.read_flags = local_read_flags; + t->parent.cancel = local_cancel; + + t->owner = owner; *out = (git_transport *) t; diff --git a/src/transports/smart.c b/src/transports/smart.c new file mode 100644 index 000000000..416eb221f --- /dev/null +++ b/src/transports/smart.c @@ -0,0 +1,345 @@ +/* + * 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 "git2.h" +#include "smart.h" +#include "refs.h" + +static int git_smart__recv_cb(gitno_buffer *buf) +{ + transport_smart *t = (transport_smart *) buf->cb_data; + size_t old_len, bytes_read; + int error; + + assert(t->current_stream); + + old_len = buf->offset; + + if ((error = t->current_stream->read(t->current_stream, buf->data + buf->offset, buf->len - buf->offset, &bytes_read)) < 0) + return error; + + buf->offset += bytes_read; + + if (t->packetsize_cb) + t->packetsize_cb(bytes_read, t->packetsize_payload); + + return (int)(buf->offset - old_len); +} + +GIT_INLINE(int) git_smart__reset_stream(transport_smart *t, bool close_subtransport) +{ + if (t->current_stream) { + t->current_stream->free(t->current_stream); + t->current_stream = NULL; + } + + if (close_subtransport && + t->wrapped->close(t->wrapped) < 0) + return -1; + + return 0; +} + +static int git_smart__set_callbacks( + git_transport *transport, + git_transport_message_cb progress_cb, + git_transport_message_cb error_cb, + void *message_cb_payload) +{ + transport_smart *t = (transport_smart *)transport; + + t->progress_cb = progress_cb; + t->error_cb = error_cb; + t->message_cb_payload = message_cb_payload; + + return 0; +} + +static int git_smart__connect( + git_transport *transport, + const char *url, + git_cred_acquire_cb cred_acquire_cb, + void *cred_acquire_payload, + int direction, + int flags) +{ + transport_smart *t = (transport_smart *)transport; + git_smart_subtransport_stream *stream; + int error; + git_pkt *pkt; + git_pkt_ref *first; + git_smart_service_t service; + + if (git_smart__reset_stream(t, true) < 0) + return -1; + + t->url = git__strdup(url); + GITERR_CHECK_ALLOC(t->url); + + t->direction = direction; + t->flags = flags; + t->cred_acquire_cb = cred_acquire_cb; + t->cred_acquire_payload = cred_acquire_payload; + + if (GIT_DIRECTION_FETCH == t->direction) + service = GIT_SERVICE_UPLOADPACK_LS; + else if (GIT_DIRECTION_PUSH == t->direction) + service = GIT_SERVICE_RECEIVEPACK_LS; + else { + giterr_set(GITERR_NET, "Invalid direction"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, service)) < 0) + return error; + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + /* 2 flushes for RPC; 1 for stateful */ + if ((error = git_smart__store_refs(t, t->rpc ? 2 : 1)) < 0) + return error; + + /* Strip the comment packet for RPC */ + if (t->rpc) { + pkt = (git_pkt *)git_vector_get(&t->refs, 0); + + if (!pkt || GIT_PKT_COMMENT != pkt->type) { + giterr_set(GITERR_NET, "Invalid response"); + return -1; + } else { + /* Remove the comment pkt from the list */ + git_vector_remove(&t->refs, 0); + git__free(pkt); + } + } + + /* We now have loaded the refs. */ + t->have_refs = 1; + + first = (git_pkt_ref *)git_vector_get(&t->refs, 0); + + /* Detect capabilities */ + if (git_smart__detect_caps(first, &t->caps) < 0) + return -1; + + /* If the only ref in the list is capabilities^{} with OID_ZERO, remove it */ + if (1 == t->refs.length && !strcmp(first->head.name, "capabilities^{}") && + git_oid_iszero(&first->head.oid)) { + git_vector_clear(&t->refs); + git_pkt_free((git_pkt *)first); + } + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + /* We're now logically connected. */ + t->connected = 1; + + return 0; +} + +static int git_smart__ls(git_transport *transport, git_headlist_cb list_cb, void *payload) +{ + transport_smart *t = (transport_smart *)transport; + unsigned int i; + git_pkt *p = NULL; + + if (!t->have_refs) { + giterr_set(GITERR_NET, "The transport has not yet loaded the refs"); + return -1; + } + + git_vector_foreach(&t->refs, i, p) { + git_pkt_ref *pkt = NULL; + + if (p->type != GIT_PKT_REF) + continue; + + pkt = (git_pkt_ref *)p; + + if (list_cb(&pkt->head, payload)) + return GIT_EUSER; + } + + return 0; +} + +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len) +{ + transport_smart *t = (transport_smart *)transport; + git_smart_subtransport_stream *stream; + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_FETCH != t->direction) { + giterr_set(GITERR_NET, "This operation is only valid for fetch"); + return -1; + } + + if ((error = t->wrapped->action(&stream, t->wrapped, t->url, GIT_SERVICE_UPLOADPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + assert(t->rpc || t->current_stream == stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = stream; + + if ((error = stream->write(stream, (const char *)data, len)) < 0) + return error; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + return 0; +} + +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **stream) +{ + int error; + + if (t->rpc && git_smart__reset_stream(t, false) < 0) + return -1; + + if (GIT_DIRECTION_PUSH != t->direction) { + giterr_set(GITERR_NET, "This operation is only valid for push"); + return -1; + } + + if ((error = t->wrapped->action(stream, t->wrapped, t->url, GIT_SERVICE_RECEIVEPACK)) < 0) + return error; + + /* If this is a stateful implementation, the stream we get back should be the same */ + assert(t->rpc || t->current_stream == *stream); + + /* Save off the current stream (i.e. socket) that we are working with */ + t->current_stream = *stream; + + gitno_buffer_setup_callback(NULL, &t->buffer, t->buffer_data, sizeof(t->buffer_data), git_smart__recv_cb, t); + + return 0; +} + +static void git_smart__cancel(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + + git_atomic_set(&t->cancelled, 1); +} + +static int git_smart__is_connected(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + + return t->connected; +} + +static int git_smart__read_flags(git_transport *transport, int *flags) +{ + transport_smart *t = (transport_smart *)transport; + + *flags = t->flags; + + return 0; +} + +static int git_smart__close(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + git_vector *common = &t->common; + unsigned int i; + git_pkt *p; + int ret; + + ret = git_smart__reset_stream(t, true); + + git_vector_foreach(common, i, p) + git_pkt_free(p); + + git_vector_free(common); + + if (t->url) { + git__free(t->url); + t->url = NULL; + } + + t->connected = 0; + + return ret; +} + +static void git_smart__free(git_transport *transport) +{ + transport_smart *t = (transport_smart *)transport; + git_vector *refs = &t->refs; + unsigned int i; + git_pkt *p; + + /* Make sure that the current stream is closed, if we have one. */ + git_smart__close(transport); + + /* Free the subtransport */ + t->wrapped->free(t->wrapped); + + git_vector_foreach(refs, i, p) + git_pkt_free(p); + + git_vector_free(refs); + + git__free(t); +} + +static int ref_name_cmp(const void *a, const void *b) +{ + const git_pkt_ref *ref_a = a, *ref_b = b; + + return strcmp(ref_a->head.name, ref_b->head.name); +} + +int git_transport_smart(git_transport **out, git_remote *owner, void *param) +{ + transport_smart *t; + git_smart_subtransport_definition *definition = (git_smart_subtransport_definition *)param; + + if (!param) + return -1; + + t = git__calloc(sizeof(transport_smart), 1); + GITERR_CHECK_ALLOC(t); + + t->parent.version = GIT_TRANSPORT_VERSION; + t->parent.set_callbacks = git_smart__set_callbacks; + t->parent.connect = git_smart__connect; + t->parent.close = git_smart__close; + t->parent.free = git_smart__free; + t->parent.negotiate_fetch = git_smart__negotiate_fetch; + t->parent.download_pack = git_smart__download_pack; + t->parent.push = git_smart__push; + t->parent.ls = git_smart__ls; + t->parent.is_connected = git_smart__is_connected; + t->parent.read_flags = git_smart__read_flags; + t->parent.cancel = git_smart__cancel; + + t->owner = owner; + t->rpc = definition->rpc; + + if (git_vector_init(&t->refs, 16, ref_name_cmp) < 0) { + git__free(t); + return -1; + } + + if (definition->callback(&t->wrapped, &t->parent) < 0) { + git__free(t); + return -1; + } + + *out = (git_transport *) t; + return 0; +} diff --git a/src/transports/smart.h b/src/transports/smart.h new file mode 100644 index 000000000..c52401a3c --- /dev/null +++ b/src/transports/smart.h @@ -0,0 +1,179 @@ +/* + * 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 "git2.h" +#include "vector.h" +#include "netops.h" +#include "buffer.h" +#include "push.h" + +#define GIT_SIDE_BAND_DATA 1 +#define GIT_SIDE_BAND_PROGRESS 2 +#define GIT_SIDE_BAND_ERROR 3 + +#define GIT_CAP_OFS_DELTA "ofs-delta" +#define GIT_CAP_MULTI_ACK "multi_ack" +#define GIT_CAP_SIDE_BAND "side-band" +#define GIT_CAP_SIDE_BAND_64K "side-band-64k" +#define GIT_CAP_INCLUDE_TAG "include-tag" +#define GIT_CAP_DELETE_REFS "delete-refs" +#define GIT_CAP_REPORT_STATUS "report-status" + +enum git_pkt_type { + GIT_PKT_CMD, + GIT_PKT_FLUSH, + GIT_PKT_REF, + GIT_PKT_HAVE, + GIT_PKT_ACK, + GIT_PKT_NAK, + GIT_PKT_PACK, + GIT_PKT_COMMENT, + GIT_PKT_ERR, + GIT_PKT_DATA, + GIT_PKT_PROGRESS, + GIT_PKT_OK, + GIT_PKT_NG, + GIT_PKT_UNPACK, +}; + +/* Used for multi-ack */ +enum git_ack_status { + GIT_ACK_NONE, + GIT_ACK_CONTINUE, + GIT_ACK_COMMON, + GIT_ACK_READY +}; + +/* This would be a flush pkt */ +typedef struct { + enum git_pkt_type type; +} git_pkt; + +struct git_pkt_cmd { + enum git_pkt_type type; + char *cmd; + char *path; + char *host; +}; + +/* This is a pkt-line with some info in it */ +typedef struct { + enum git_pkt_type type; + git_remote_head head; + char *capabilities; +} git_pkt_ref; + +/* Useful later */ +typedef struct { + enum git_pkt_type type; + git_oid oid; + enum git_ack_status status; +} git_pkt_ack; + +typedef struct { + enum git_pkt_type type; + char comment[GIT_FLEX_ARRAY]; +} git_pkt_comment; + +typedef struct { + enum git_pkt_type type; + int len; + char data[GIT_FLEX_ARRAY]; +} git_pkt_data; + +typedef git_pkt_data git_pkt_progress; + +typedef struct { + enum git_pkt_type type; + int len; + char error[GIT_FLEX_ARRAY]; +} git_pkt_err; + +typedef struct { + enum git_pkt_type type; + char *ref; +} git_pkt_ok; + +typedef struct { + enum git_pkt_type type; + char *ref; + char *msg; +} git_pkt_ng; + +typedef struct { + enum git_pkt_type type; + int unpack_ok; +} git_pkt_unpack; + +typedef struct transport_smart_caps { + int common:1, + ofs_delta:1, + multi_ack: 1, + side_band:1, + side_band_64k:1, + include_tag:1, + delete_refs:1, + report_status:1; +} transport_smart_caps; + +typedef void (*packetsize_cb)(size_t received, void *payload); + +typedef struct { + git_transport parent; + git_remote *owner; + char *url; + git_cred_acquire_cb cred_acquire_cb; + void *cred_acquire_payload; + int direction; + int flags; + git_transport_message_cb progress_cb; + git_transport_message_cb error_cb; + void *message_cb_payload; + git_smart_subtransport *wrapped; + git_smart_subtransport_stream *current_stream; + transport_smart_caps caps; + git_vector refs; + git_vector common; + git_atomic cancelled; + packetsize_cb packetsize_cb; + void *packetsize_payload; + unsigned rpc : 1, + have_refs : 1, + connected : 1; + gitno_buffer buffer; + char buffer_data[65536]; +} transport_smart; + +/* smart_protocol.c */ +int git_smart__store_refs(transport_smart *t, int flushes); +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps); +int git_smart__push(git_transport *transport, git_push *push); + +int git_smart__negotiate_fetch( + git_transport *transport, + git_repository *repo, + const git_remote_head * const *refs, + size_t count); + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload); + +/* smart.c */ +int git_smart__negotiation_step(git_transport *transport, void *data, size_t len); +int git_smart__get_push_stream(transport_smart *t, git_smart_subtransport_stream **out); + +/* smart_pkt.c */ +int git_pkt_parse_line(git_pkt **head, const char *line, const char **out, size_t len); +int git_pkt_buffer_flush(git_buf *buf); +int git_pkt_send_flush(GIT_SOCKET s); +int git_pkt_buffer_done(git_buf *buf); +int git_pkt_buffer_wants(const git_remote_head * const *refs, size_t count, transport_smart_caps *caps, git_buf *buf); +int git_pkt_buffer_have(git_oid *oid, git_buf *buf); +void git_pkt_free(git_pkt *pkt); diff --git a/src/transports/smart_pkt.c b/src/transports/smart_pkt.c new file mode 100644 index 000000000..99da37567 --- /dev/null +++ b/src/transports/smart_pkt.c @@ -0,0 +1,546 @@ +/* + * 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 "common.h" + +#include "git2/types.h" +#include "git2/errors.h" +#include "git2/refs.h" +#include "git2/revwalk.h" + +#include "smart.h" +#include "util.h" +#include "netops.h" +#include "posix.h" +#include "buffer.h" + +#include <ctype.h> + +#define PKT_LEN_SIZE 4 +static const char pkt_done_str[] = "0009done\n"; +static const char pkt_flush_str[] = "0000"; +static const char pkt_have_prefix[] = "0032have "; +static const char pkt_want_prefix[] = "0032want "; + +static int flush_pkt(git_pkt **out) +{ + git_pkt *pkt; + + pkt = git__malloc(sizeof(git_pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_FLUSH; + *out = pkt; + + return 0; +} + +/* the rest of the line will be useful for multi_ack */ +static int ack_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ack *pkt; + GIT_UNUSED(line); + GIT_UNUSED(len); + + pkt = git__calloc(1, sizeof(git_pkt_ack)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_ACK; + line += 3; + len -= 3; + + if (len >= GIT_OID_HEXSZ) { + git_oid_fromstr(&pkt->oid, line + 1); + line += GIT_OID_HEXSZ + 1; + len -= GIT_OID_HEXSZ + 1; + } + + if (len >= 7) { + if (!git__prefixcmp(line + 1, "continue")) + pkt->status = GIT_ACK_CONTINUE; + } + + *out = (git_pkt *) pkt; + + return 0; +} + +static int nak_pkt(git_pkt **out) +{ + git_pkt *pkt; + + pkt = git__malloc(sizeof(git_pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_NAK; + *out = pkt; + + return 0; +} + +static int pack_pkt(git_pkt **out) +{ + git_pkt *pkt; + + pkt = git__malloc(sizeof(git_pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_PACK; + *out = pkt; + + return 0; +} + +static int comment_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_comment *pkt; + + pkt = git__malloc(sizeof(git_pkt_comment) + len + 1); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_COMMENT; + memcpy(pkt->comment, line, len); + pkt->comment[len] = '\0'; + + *out = (git_pkt *) pkt; + + return 0; +} + +static int err_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_err *pkt; + + /* Remove "ERR " from the line */ + line += 4; + len -= 4; + pkt = git__malloc(sizeof(git_pkt_err) + len + 1); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_ERR; + pkt->len = (int)len; + memcpy(pkt->error, line, len); + pkt->error[len] = '\0'; + + *out = (git_pkt *) pkt; + + return 0; +} + +static int data_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_data *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_data) + len); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_DATA; + pkt->len = (int) len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int progress_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_progress *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_progress) + len); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_PROGRESS; + pkt->len = (int) len; + memcpy(pkt->data, line, len); + + *out = (git_pkt *) pkt; + + return 0; +} + +static int sideband_error_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_err *pkt; + + line++; + len--; + pkt = git__malloc(sizeof(git_pkt_err) + len + 1); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_ERR; + pkt->len = (int)len; + memcpy(pkt->error, line, len); + pkt->error[len] = '\0'; + + *out = (git_pkt *)pkt; + + return 0; +} + +/* + * Parse an other-ref line. + */ +static int ref_pkt(git_pkt **out, const char *line, size_t len) +{ + int error; + git_pkt_ref *pkt; + + pkt = git__malloc(sizeof(git_pkt_ref)); + GITERR_CHECK_ALLOC(pkt); + + memset(pkt, 0x0, sizeof(git_pkt_ref)); + pkt->type = GIT_PKT_REF; + if ((error = git_oid_fromstr(&pkt->head.oid, line)) < 0) + goto error_out; + + /* Check for a bit of consistency */ + if (line[GIT_OID_HEXSZ] != ' ') { + giterr_set(GITERR_NET, "Error parsing pkt-line"); + error = -1; + goto error_out; + } + + /* Jump from the name */ + line += GIT_OID_HEXSZ + 1; + len -= (GIT_OID_HEXSZ + 1); + + if (line[len - 1] == '\n') + --len; + + pkt->head.name = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->head.name); + + memcpy(pkt->head.name, line, len); + pkt->head.name[len] = '\0'; + + if (strlen(pkt->head.name) < len) { + pkt->capabilities = strchr(pkt->head.name, '\0') + 1; + } + + *out = (git_pkt *)pkt; + return 0; + +error_out: + git__free(pkt); + return error; +} + +static int ok_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ok *pkt; + const char *ptr; + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_OK; + + line += 3; /* skip "ok " */ + ptr = strchr(line, '\n'); + len = ptr - line; + + pkt->ref = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; +} + +static int ng_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_ng *pkt; + const char *ptr; + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_NG; + + line += 3; /* skip "ng " */ + ptr = strchr(line, ' '); + len = ptr - line; + + pkt->ref = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->ref); + + memcpy(pkt->ref, line, len); + pkt->ref[len] = '\0'; + + line = ptr + 1; + ptr = strchr(line, '\n'); + len = ptr - line; + + pkt->msg = git__malloc(len + 1); + GITERR_CHECK_ALLOC(pkt->msg); + + memcpy(pkt->msg, line, len); + pkt->msg[len] = '\0'; + + *out = (git_pkt *)pkt; + return 0; +} + +static int unpack_pkt(git_pkt **out, const char *line, size_t len) +{ + git_pkt_unpack *pkt; + + GIT_UNUSED(len); + + pkt = git__malloc(sizeof(*pkt)); + GITERR_CHECK_ALLOC(pkt); + + pkt->type = GIT_PKT_UNPACK; + if (!git__prefixcmp(line, "unpack ok")) + pkt->unpack_ok = 1; + else + pkt->unpack_ok = 0; + + *out = (git_pkt *)pkt; + return 0; +} + +static int32_t parse_len(const char *line) +{ + char num[PKT_LEN_SIZE + 1]; + int i, error; + int32_t len; + const char *num_end; + + memcpy(num, line, PKT_LEN_SIZE); + num[PKT_LEN_SIZE] = '\0'; + + for (i = 0; i < PKT_LEN_SIZE; ++i) { + if (!isxdigit(num[i])) { + giterr_set(GITERR_NET, "Found invalid hex digit in length"); + return -1; + } + } + + if ((error = git__strtol32(&len, num, &num_end, 16)) < 0) + return error; + + return len; +} + +/* + * As per the documentation, the syntax is: + * + * pkt-line = data-pkt / flush-pkt + * data-pkt = pkt-len pkt-payload + * pkt-len = 4*(HEXDIG) + * pkt-payload = (pkt-len -4)*(OCTET) + * flush-pkt = "0000" + * + * Which means that the first four bytes are the length of the line, + * in ASCII hexadecimal (including itself) + */ + +int git_pkt_parse_line( + git_pkt **head, const char *line, const char **out, size_t bufflen) +{ + int ret; + int32_t len; + + /* Not even enough for the length */ + if (bufflen > 0 && bufflen < PKT_LEN_SIZE) + return GIT_EBUFS; + + len = parse_len(line); + if (len < 0) { + /* + * If we fail to parse the length, it might be because the + * server is trying to send us the packfile already. + */ + if (bufflen >= 4 && !git__prefixcmp(line, "PACK")) { + giterr_clear(); + *out = line; + return pack_pkt(head); + } + + return (int)len; + } + + /* + * If we were given a buffer length, then make sure there is + * enough in the buffer to satisfy this line + */ + if (bufflen > 0 && bufflen < (size_t)len) + return GIT_EBUFS; + + line += PKT_LEN_SIZE; + /* + * TODO: How do we deal with empty lines? Try again? with the next + * line? + */ + if (len == PKT_LEN_SIZE) { + *out = line; + return 0; + } + + if (len == 0) { /* Flush pkt */ + *out = line; + return flush_pkt(head); + } + + len -= PKT_LEN_SIZE; /* the encoded length includes its own size */ + + if (*line == GIT_SIDE_BAND_DATA) + ret = data_pkt(head, line, len); + else if (*line == GIT_SIDE_BAND_PROGRESS) + ret = progress_pkt(head, line, len); + else if (*line == GIT_SIDE_BAND_ERROR) + ret = sideband_error_pkt(head, line, len); + else if (!git__prefixcmp(line, "ACK")) + ret = ack_pkt(head, line, len); + else if (!git__prefixcmp(line, "NAK")) + ret = nak_pkt(head); + else if (!git__prefixcmp(line, "ERR ")) + ret = err_pkt(head, line, len); + else if (*line == '#') + ret = comment_pkt(head, line, len); + else if (!git__prefixcmp(line, "ok")) + ret = ok_pkt(head, line, len); + else if (!git__prefixcmp(line, "ng")) + ret = ng_pkt(head, line, len); + else if (!git__prefixcmp(line, "unpack")) + ret = unpack_pkt(head, line, len); + else + ret = ref_pkt(head, line, len); + + *out = line + len; + + return ret; +} + +void git_pkt_free(git_pkt *pkt) +{ + if (pkt->type == GIT_PKT_REF) { + git_pkt_ref *p = (git_pkt_ref *) pkt; + git__free(p->head.name); + } + + if (pkt->type == GIT_PKT_OK) { + git_pkt_ok *p = (git_pkt_ok *) pkt; + git__free(p->ref); + } + + if (pkt->type == GIT_PKT_NG) { + git_pkt_ng *p = (git_pkt_ng *) pkt; + git__free(p->ref); + git__free(p->msg); + } + + git__free(pkt); +} + +int git_pkt_buffer_flush(git_buf *buf) +{ + return git_buf_put(buf, pkt_flush_str, strlen(pkt_flush_str)); +} + +static int buffer_want_with_caps(const git_remote_head *head, transport_smart_caps *caps, git_buf *buf) +{ + git_buf str = GIT_BUF_INIT; + char oid[GIT_OID_HEXSZ +1] = {0}; + unsigned int len; + + /* Prefer side-band-64k if the server supports both */ + if (caps->side_band) { + if (caps->side_band_64k) + git_buf_printf(&str, "%s ", GIT_CAP_SIDE_BAND_64K); + else + git_buf_printf(&str, "%s ", GIT_CAP_SIDE_BAND); + } + if (caps->ofs_delta) + git_buf_puts(&str, GIT_CAP_OFS_DELTA " "); + + if (caps->multi_ack) + git_buf_puts(&str, GIT_CAP_MULTI_ACK " "); + + if (caps->include_tag) + git_buf_puts(&str, GIT_CAP_INCLUDE_TAG " "); + + if (git_buf_oom(&str)) + return -1; + + len = (unsigned int) + (strlen("XXXXwant ") + GIT_OID_HEXSZ + 1 /* NUL */ + + git_buf_len(&str) + 1 /* LF */); + git_buf_grow(buf, git_buf_len(buf) + len); + git_oid_fmt(oid, &head->oid); + git_buf_printf(buf, "%04xwant %s %s\n", len, oid, git_buf_cstr(&str)); + git_buf_free(&str); + + return git_buf_oom(buf); +} + +/* + * All "want" packets have the same length and format, so what we do + * is overwrite the OID each time. + */ + +int git_pkt_buffer_wants( + const git_remote_head * const *refs, + size_t count, + transport_smart_caps *caps, + git_buf *buf) +{ + size_t i = 0; + const git_remote_head *head; + + if (caps->common) { + for (; i < count; ++i) { + head = refs[i]; + if (!head->local) + break; + } + + if (buffer_want_with_caps(refs[i], caps, buf) < 0) + return -1; + + i++; + } + + for (; i < count; ++i) { + char oid[GIT_OID_HEXSZ]; + + head = refs[i]; + if (head->local) + continue; + + git_oid_fmt(oid, &head->oid); + git_buf_put(buf, pkt_want_prefix, strlen(pkt_want_prefix)); + git_buf_put(buf, oid, GIT_OID_HEXSZ); + git_buf_putc(buf, '\n'); + if (git_buf_oom(buf)) + return -1; + } + + return git_pkt_buffer_flush(buf); +} + +int git_pkt_buffer_have(git_oid *oid, git_buf *buf) +{ + char oidhex[GIT_OID_HEXSZ + 1]; + + memset(oidhex, 0x0, sizeof(oidhex)); + git_oid_fmt(oidhex, oid); + return git_buf_printf(buf, "%s%s\n", pkt_have_prefix, oidhex); +} + +int git_pkt_buffer_done(git_buf *buf) +{ + return git_buf_puts(buf, pkt_done_str); +} diff --git a/src/transports/smart_protocol.c b/src/transports/smart_protocol.c new file mode 100644 index 000000000..8acedeb49 --- /dev/null +++ b/src/transports/smart_protocol.c @@ -0,0 +1,856 @@ +/* + * 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 "git2.h" + +#include "smart.h" +#include "refs.h" +#include "repository.h" +#include "push.h" +#include "pack-objects.h" +#include "remote.h" + +#define NETWORK_XFER_THRESHOLD (100*1024) + +int git_smart__store_refs(transport_smart *t, int flushes) +{ + gitno_buffer *buf = &t->buffer; + git_vector *refs = &t->refs; + int error, flush = 0, recvd; + const char *line_end; + git_pkt *pkt; + + /* Clear existing refs in case git_remote_connect() is called again + * after git_remote_disconnect(). + */ + git_vector_clear(refs); + + do { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, buf->data, &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if (error == GIT_EBUFS) { + if ((recvd = gitno_recv(buf)) < 0) + return -1; + + if (recvd == 0 && !flush) { + giterr_set(GITERR_NET, "Early EOF"); + return -1; + } + + continue; + } + + gitno_consume(buf, line_end); + if (pkt->type == GIT_PKT_ERR) { + giterr_set(GITERR_NET, "Remote error: %s", ((git_pkt_err *)pkt)->error); + git__free(pkt); + return -1; + } + + if (pkt->type != GIT_PKT_FLUSH && git_vector_insert(refs, pkt) < 0) + return -1; + + if (pkt->type == GIT_PKT_FLUSH) { + flush++; + git_pkt_free(pkt); + } + } while (flush < flushes); + + return flush; +} + +int git_smart__detect_caps(git_pkt_ref *pkt, transport_smart_caps *caps) +{ + const char *ptr; + + /* No refs or capabilites, odd but not a problem */ + if (pkt == NULL || pkt->capabilities == NULL) + return 0; + + ptr = pkt->capabilities; + while (ptr != NULL && *ptr != '\0') { + if (*ptr == ' ') + ptr++; + + if (!git__prefixcmp(ptr, GIT_CAP_OFS_DELTA)) { + caps->common = caps->ofs_delta = 1; + ptr += strlen(GIT_CAP_OFS_DELTA); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_MULTI_ACK)) { + caps->common = caps->multi_ack = 1; + ptr += strlen(GIT_CAP_MULTI_ACK); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_INCLUDE_TAG)) { + caps->common = caps->include_tag = 1; + ptr += strlen(GIT_CAP_INCLUDE_TAG); + continue; + } + + /* Keep side-band check after side-band-64k */ + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND_64K)) { + caps->common = caps->side_band_64k = 1; + ptr += strlen(GIT_CAP_SIDE_BAND_64K); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_SIDE_BAND)) { + caps->common = caps->side_band = 1; + ptr += strlen(GIT_CAP_SIDE_BAND); + continue; + } + + if (!git__prefixcmp(ptr, GIT_CAP_DELETE_REFS)) { + caps->common = caps->delete_refs = 1; + ptr += strlen(GIT_CAP_DELETE_REFS); + continue; + } + + /* We don't know this capability, so skip it */ + ptr = strchr(ptr, ' '); + } + + return 0; +} + +static int recv_pkt(git_pkt **out, gitno_buffer *buf) +{ + const char *ptr = buf->data, *line_end = ptr; + git_pkt *pkt; + int pkt_type, error = 0, ret; + + do { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, ptr, &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error == 0) + break; /* return the pkt */ + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if ((ret = gitno_recv(buf)) < 0) + return -1; + } while (error); + + gitno_consume(buf, line_end); + pkt_type = pkt->type; + if (out != NULL) + *out = pkt; + else + git__free(pkt); + + return pkt_type; +} + +static int store_common(transport_smart *t) +{ + git_pkt *pkt = NULL; + gitno_buffer *buf = &t->buffer; + + do { + if (recv_pkt(&pkt, buf) < 0) + return -1; + + if (pkt->type == GIT_PKT_ACK) { + if (git_vector_insert(&t->common, pkt) < 0) + return -1; + } else { + git__free(pkt); + return 0; + } + + } while (1); + + return 0; +} + +static int fetch_setup_walk(git_revwalk **out, git_repository *repo) +{ + git_revwalk *walk; + git_strarray refs; + unsigned int i; + git_reference *ref; + + if (git_reference_list(&refs, repo, GIT_REF_LISTALL) < 0) + return -1; + + if (git_revwalk_new(&walk, repo) < 0) + return -1; + + git_revwalk_sorting(walk, GIT_SORT_TIME); + + for (i = 0; i < refs.count; ++i) { + /* No tags */ + if (!git__prefixcmp(refs.strings[i], GIT_REFS_TAGS_DIR)) + continue; + + if (git_reference_lookup(&ref, repo, refs.strings[i]) < 0) + goto on_error; + + if (git_reference_type(ref) == GIT_REF_SYMBOLIC) + continue; + if (git_revwalk_push(walk, git_reference_target(ref)) < 0) + goto on_error; + + git_reference_free(ref); + } + + git_strarray_free(&refs); + *out = walk; + return 0; + +on_error: + git_reference_free(ref); + git_strarray_free(&refs); + return -1; +} + +int git_smart__negotiate_fetch(git_transport *transport, git_repository *repo, const git_remote_head * const *refs, size_t count) +{ + transport_smart *t = (transport_smart *)transport; + gitno_buffer *buf = &t->buffer; + git_buf data = GIT_BUF_INIT; + git_revwalk *walk = NULL; + int error = -1, pkt_type; + unsigned int i; + git_oid oid; + + /* No own logic, do our thing */ + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + return error; + + if ((error = fetch_setup_walk(&walk, repo)) < 0) + goto on_error; + /* + * We don't support any kind of ACK extensions, so the negotiation + * boils down to sending what we have and listening for an ACK + * every once in a while. + */ + i = 0; + while (true) { + error = git_revwalk_next(&oid, walk); + + if (error < 0) { + if (GIT_ITEROVER == error) + break; + + goto on_error; + } + + git_pkt_buffer_have(&oid, &data); + i++; + if (i % 20 == 0) { + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + + git_pkt_buffer_flush(&data); + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_buf_clear(&data); + if (t->caps.multi_ack) { + if ((error = store_common(t)) < 0) + goto on_error; + } else { + pkt_type = recv_pkt(NULL, buf); + + if (pkt_type == GIT_PKT_ACK) { + break; + } else if (pkt_type == GIT_PKT_NAK) { + continue; + } else if (pkt_type < 0) { + /* recv_pkt returned an error */ + error = pkt_type; + goto on_error; + } else { + giterr_set(GITERR_NET, "Unexpected pkt type"); + error = -1; + goto on_error; + } + } + } + + if (t->common.length > 0) + break; + + if (i % 20 == 0 && t->rpc) { + git_pkt_ack *pkt; + unsigned int i; + + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, i, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + } + } + + /* Tell the other end that we're done negotiating */ + if (t->rpc && t->common.length > 0) { + git_pkt_ack *pkt; + unsigned int i; + + if ((error = git_pkt_buffer_wants(refs, count, &t->caps, &data)) < 0) + goto on_error; + + git_vector_foreach(&t->common, i, pkt) { + if ((error = git_pkt_buffer_have(&pkt->oid, &data)) < 0) + goto on_error; + } + + if (git_buf_oom(&data)) { + error = -1; + goto on_error; + } + } + + if ((error = git_pkt_buffer_done(&data)) < 0) + goto on_error; + + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + if ((error = git_smart__negotiation_step(&t->parent, data.ptr, data.size)) < 0) + goto on_error; + + git_buf_free(&data); + git_revwalk_free(walk); + + /* Now let's eat up whatever the server gives us */ + if (!t->caps.multi_ack) { + pkt_type = recv_pkt(NULL, buf); + + if (pkt_type < 0) { + return pkt_type; + } else if (pkt_type != GIT_PKT_ACK && pkt_type != GIT_PKT_NAK) { + giterr_set(GITERR_NET, "Unexpected pkt type"); + return -1; + } + } else { + git_pkt_ack *pkt; + do { + if ((error = recv_pkt((git_pkt **)&pkt, buf)) < 0) + return error; + + if (pkt->type == GIT_PKT_NAK || + (pkt->type == GIT_PKT_ACK && pkt->status != GIT_ACK_CONTINUE)) { + git__free(pkt); + break; + } + + git__free(pkt); + } while (1); + } + + return 0; + +on_error: + git_revwalk_free(walk); + git_buf_free(&data); + return error; +} + +static int no_sideband(transport_smart *t, struct git_odb_writepack *writepack, gitno_buffer *buf, git_transfer_progress *stats) +{ + int recvd; + + do { + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + return GIT_EUSER; + } + + if (writepack->add(writepack, buf->data, buf->offset, stats) < 0) + return -1; + + gitno_consume_n(buf, buf->offset); + + if ((recvd = gitno_recv(buf)) < 0) + return -1; + } while(recvd > 0); + + if (writepack->commit(writepack, stats)) + return -1; + + return 0; +} + +struct network_packetsize_payload +{ + git_transfer_progress_callback callback; + void *payload; + git_transfer_progress *stats; + size_t last_fired_bytes; +}; + +static void network_packetsize(size_t received, void *payload) +{ + struct network_packetsize_payload *npp = (struct network_packetsize_payload*)payload; + + /* Accumulate bytes */ + npp->stats->received_bytes += received; + + /* Fire notification if the threshold is reached */ + if ((npp->stats->received_bytes - npp->last_fired_bytes) > NETWORK_XFER_THRESHOLD) { + npp->last_fired_bytes = npp->stats->received_bytes; + npp->callback(npp->stats, npp->payload); + } +} + +int git_smart__download_pack( + git_transport *transport, + git_repository *repo, + git_transfer_progress *stats, + git_transfer_progress_callback progress_cb, + void *progress_payload) +{ + transport_smart *t = (transport_smart *)transport; + gitno_buffer *buf = &t->buffer; + git_odb *odb; + struct git_odb_writepack *writepack = NULL; + int error = -1; + struct network_packetsize_payload npp = {0}; + + memset(stats, 0, sizeof(git_transfer_progress)); + + if (progress_cb) { + npp.callback = progress_cb; + npp.payload = progress_payload; + npp.stats = stats; + t->packetsize_cb = &network_packetsize; + t->packetsize_payload = &npp; + + /* We might have something in the buffer already from negotiate_fetch */ + if (t->buffer.offset > 0) + t->packetsize_cb(t->buffer.offset, t->packetsize_payload); + } + + if ((error = git_repository_odb__weakptr(&odb, repo)) < 0 || + ((error = git_odb_write_pack(&writepack, odb, progress_cb, progress_payload)) < 0)) + goto on_error; + + /* + * If the remote doesn't support the side-band, we can feed + * the data directly to the pack writer. Otherwise, we need to + * check which one belongs there. + */ + if (!t->caps.side_band && !t->caps.side_band_64k) { + if (no_sideband(t, writepack, buf, stats) < 0) + goto on_error; + + goto on_success; + } + + do { + git_pkt *pkt; + + if (t->cancelled.val) { + giterr_set(GITERR_NET, "The fetch was cancelled by the user"); + error = GIT_EUSER; + goto on_error; + } + + if (recv_pkt(&pkt, buf) < 0) + goto on_error; + + if (pkt->type == GIT_PKT_PROGRESS) { + if (t->progress_cb) { + git_pkt_progress *p = (git_pkt_progress *) pkt; + t->progress_cb(p->data, p->len, t->message_cb_payload); + } + git__free(pkt); + } else if (pkt->type == GIT_PKT_DATA) { + git_pkt_data *p = (git_pkt_data *) pkt; + error = writepack->add(writepack, p->data, p->len, stats); + + git__free(pkt); + if (error < 0) + goto on_error; + } else if (pkt->type == GIT_PKT_FLUSH) { + /* A flush indicates the end of the packfile */ + git__free(pkt); + break; + } + } while (1); + + if (writepack->commit(writepack, stats) < 0) + goto on_error; + +on_success: + error = 0; + +on_error: + if (writepack) + writepack->free(writepack); + + /* Trailing execution of progress_cb, if necessary */ + if (npp.callback && npp.stats->received_bytes > npp.last_fired_bytes) + npp.callback(npp.stats, npp.payload); + + return error; +} + +static int gen_pktline(git_buf *buf, git_push *push) +{ + push_spec *spec; + size_t i, len; + char old_id[41], new_id[41]; + + old_id[40] = '\0'; new_id[40] = '\0'; + + git_vector_foreach(&push->specs, i, spec) { + len = 2*GIT_OID_HEXSZ + 7 + strlen(spec->rref); + + if (i == 0) { + ++len; /* '\0' */ + if (push->report_status) + len += strlen(GIT_CAP_REPORT_STATUS) + 1; + len += strlen(GIT_CAP_SIDE_BAND_64K) + 1; + } + + git_oid_fmt(old_id, &spec->roid); + git_oid_fmt(new_id, &spec->loid); + + git_buf_printf(buf, "%04"PRIxZ"%s %s %s", len, old_id, new_id, spec->rref); + + if (i == 0) { + git_buf_putc(buf, '\0'); + /* Core git always starts their capabilities string with a space */ + if (push->report_status) { + git_buf_putc(buf, ' '); + git_buf_printf(buf, GIT_CAP_REPORT_STATUS); + } + git_buf_putc(buf, ' '); + git_buf_printf(buf, GIT_CAP_SIDE_BAND_64K); + } + + git_buf_putc(buf, '\n'); + } + + git_buf_puts(buf, "0000"); + return git_buf_oom(buf) ? -1 : 0; +} + +static int add_push_report_pkt(git_push *push, git_pkt *pkt) +{ + push_status *status; + + switch (pkt->type) { + case GIT_PKT_OK: + status = git__malloc(sizeof(push_status)); + GITERR_CHECK_ALLOC(status); + status->msg = NULL; + status->ref = git__strdup(((git_pkt_ok *)pkt)->ref); + if (!status->ref || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_NG: + status = git__calloc(sizeof(push_status), 1); + GITERR_CHECK_ALLOC(status); + status->ref = git__strdup(((git_pkt_ng *)pkt)->ref); + status->msg = git__strdup(((git_pkt_ng *)pkt)->msg); + if (!status->ref || !status->msg || + git_vector_insert(&push->status, status) < 0) { + git_push_status_free(status); + return -1; + } + break; + case GIT_PKT_UNPACK: + push->unpack_ok = ((git_pkt_unpack *)pkt)->unpack_ok; + break; + case GIT_PKT_FLUSH: + return GIT_ITEROVER; + default: + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + + return 0; +} + +static int add_push_report_sideband_pkt(git_push *push, git_pkt_data *data_pkt) +{ + git_pkt *pkt; + const char *line = data_pkt->data, *line_end; + size_t line_len = data_pkt->len; + int error; + + while (line_len > 0) { + error = git_pkt_parse_line(&pkt, line, &line_end, line_len); + + if (error < 0) + return error; + + /* Advance in the buffer */ + line_len -= (line_end - line); + line = line_end; + + error = add_push_report_pkt(push, pkt); + + git_pkt_free(pkt); + + if (error < 0 && error != GIT_ITEROVER) + return error; + } + + return 0; +} + +static int parse_report(gitno_buffer *buf, git_push *push) +{ + git_pkt *pkt; + const char *line_end; + int error, recvd; + + for (;;) { + if (buf->offset > 0) + error = git_pkt_parse_line(&pkt, buf->data, + &line_end, buf->offset); + else + error = GIT_EBUFS; + + if (error < 0 && error != GIT_EBUFS) + return -1; + + if (error == GIT_EBUFS) { + if ((recvd = gitno_recv(buf)) < 0) + return -1; + + if (recvd == 0) { + giterr_set(GITERR_NET, "Early EOF"); + return -1; + } + continue; + } + + gitno_consume(buf, line_end); + + error = 0; + + switch (pkt->type) { + case GIT_PKT_DATA: + /* This is a sideband packet which contains other packets */ + error = add_push_report_sideband_pkt(push, (git_pkt_data *)pkt); + break; + case GIT_PKT_ERR: + giterr_set(GITERR_NET, "report-status: Error reported: %s", + ((git_pkt_err *)pkt)->error); + error = -1; + break; + case GIT_PKT_PROGRESS: + break; + default: + error = add_push_report_pkt(push, pkt); + break; + } + + git_pkt_free(pkt); + + /* add_push_report_pkt returns GIT_ITEROVER when it receives a flush */ + if (error == GIT_ITEROVER) + return 0; + + if (error < 0) + return error; + } +} + +static int add_ref_from_push_spec(git_vector *refs, push_spec *push_spec) +{ + git_pkt_ref *added = git__calloc(1, sizeof(git_pkt_ref)); + GITERR_CHECK_ALLOC(added); + + added->type = GIT_PKT_REF; + git_oid_cpy(&added->head.oid, &push_spec->loid); + added->head.name = git__strdup(push_spec->rref); + + if (!added->head.name || + git_vector_insert(refs, added) < 0) { + git_pkt_free((git_pkt *)added); + return -1; + } + + return 0; +} + +static int update_refs_from_report( + git_vector *refs, + git_vector *push_specs, + git_vector *push_report) +{ + git_pkt_ref *ref; + push_spec *push_spec; + push_status *push_status; + size_t i, j, refs_len; + int cmp; + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report */ + if (push_specs->length != push_report->length) { + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + + /* We require that push_specs be sorted with push_spec_rref_cmp, + * and that push_report be sorted with push_status_ref_cmp */ + git_vector_sort(push_specs); + git_vector_sort(push_report); + + git_vector_foreach(push_specs, i, push_spec) { + push_status = git_vector_get(push_report, i); + + /* For each push spec we sent to the server, we should have + * gotten back a status packet in the push report which matches */ + if (strcmp(push_spec->rref, push_status->ref)) { + giterr_set(GITERR_NET, "report-status: protocol error"); + return -1; + } + } + + /* We require that refs be sorted with ref_name_cmp */ + git_vector_sort(refs); + i = j = 0; + refs_len = refs->length; + + /* Merge join push_specs with refs */ + while (i < push_specs->length && j < refs_len) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + ref = git_vector_get(refs, j); + + cmp = strcmp(push_spec->rref, ref->head.name); + + /* Iterate appropriately */ + if (cmp <= 0) i++; + if (cmp >= 0) j++; + + /* Add case */ + if (cmp < 0 && + !push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + + /* Update case, delete case */ + if (cmp == 0 && + !push_status->msg) + git_oid_cpy(&ref->head.oid, &push_spec->loid); + } + + for (; i < push_specs->length; i++) { + push_spec = git_vector_get(push_specs, i); + push_status = git_vector_get(push_report, i); + + /* Add case */ + if (!push_status->msg && + add_ref_from_push_spec(refs, push_spec) < 0) + return -1; + } + + /* Remove any refs which we updated to have a zero OID. */ + git_vector_rforeach(refs, i, ref) { + if (git_oid_iszero(&ref->head.oid)) { + git_vector_remove(refs, i); + git_pkt_free((git_pkt *)ref); + } + } + + git_vector_sort(refs); + + return 0; +} + +static int stream_thunk(void *buf, size_t size, void *data) +{ + git_smart_subtransport_stream *s = (git_smart_subtransport_stream *)data; + + return s->write(s, (const char *)buf, size); +} + +int git_smart__push(git_transport *transport, git_push *push) +{ + transport_smart *t = (transport_smart *)transport; + git_smart_subtransport_stream *s; + git_buf pktline = GIT_BUF_INIT; + int error = -1; + +#ifdef PUSH_DEBUG +{ + git_remote_head *head; + push_spec *spec; + unsigned int i; + char hex[41]; hex[40] = '\0'; + + git_vector_foreach(&push->remote->refs, i, head) { + git_oid_fmt(hex, &head->oid); + fprintf(stderr, "%s (%s)\n", hex, head->name); + } + + git_vector_foreach(&push->specs, i, spec) { + git_oid_fmt(hex, &spec->roid); + fprintf(stderr, "%s (%s) -> ", hex, spec->lref); + git_oid_fmt(hex, &spec->loid); + fprintf(stderr, "%s (%s)\n", hex, spec->rref ? + spec->rref : spec->lref); + } +} +#endif + + if (git_smart__get_push_stream(t, &s) < 0 || + gen_pktline(&pktline, push) < 0 || + s->write(s, git_buf_cstr(&pktline), git_buf_len(&pktline)) < 0 || + git_packbuilder_foreach(push->pb, &stream_thunk, s) < 0) + goto on_error; + + /* If we sent nothing or the server doesn't support report-status, then + * we consider the pack to have been unpacked successfully */ + if (!push->specs.length || !push->report_status) + push->unpack_ok = 1; + else if (parse_report(&t->buffer, push) < 0) + goto on_error; + + if (push->status.length && + update_refs_from_report(&t->refs, &push->specs, &push->status) < 0) + goto on_error; + + error = 0; + +on_error: + git_buf_free(&pktline); + + return error; +} diff --git a/src/transports/winhttp.c b/src/transports/winhttp.c new file mode 100644 index 000000000..e502001cb --- /dev/null +++ b/src/transports/winhttp.c @@ -0,0 +1,1136 @@ +/* + * 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. + */ + +#ifdef GIT_WINHTTP + +#include "git2.h" +#include "git2/transport.h" +#include "buffer.h" +#include "posix.h" +#include "netops.h" +#include "smart.h" +#include "remote.h" +#include "repository.h" + +#include <winhttp.h> +#pragma comment(lib, "winhttp") + +/* For UuidCreate */ +#pragma comment(lib, "rpcrt4") + +#define WIDEN2(s) L ## s +#define WIDEN(s) WIDEN2(s) + +#define MAX_CONTENT_TYPE_LEN 100 +#define WINHTTP_OPTION_PEERDIST_EXTENSION_STATE 109 +#define CACHED_POST_BODY_BUF_SIZE 4096 +#define UUID_LENGTH_CCH 32 + +static const char *prefix_http = "http://"; +static const char *prefix_https = "https://"; +static const char *upload_pack_service = "upload-pack"; +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_service = "receive-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; +static const wchar_t *get_verb = L"GET"; +static const wchar_t *post_verb = L"POST"; +static const wchar_t *pragma_nocache = L"Pragma: no-cache"; +static const wchar_t *transfer_encoding = L"Transfer-Encoding: chunked"; +static const int no_check_cert_flags = SECURITY_FLAG_IGNORE_CERT_CN_INVALID | + SECURITY_FLAG_IGNORE_CERT_DATE_INVALID | + SECURITY_FLAG_IGNORE_UNKNOWN_CA; + +#define OWNING_SUBTRANSPORT(s) ((winhttp_subtransport *)(s)->parent.subtransport) + +typedef enum { + GIT_WINHTTP_AUTH_BASIC = 1, +} winhttp_authmechanism_t; + +typedef struct { + git_smart_subtransport_stream parent; + const char *service; + const char *service_url; + const wchar_t *verb; + HINTERNET request; + wchar_t *request_uri; + char *chunk_buffer; + unsigned chunk_buffer_len; + HANDLE post_body; + DWORD post_body_len; + unsigned sent_request : 1, + received_response : 1, + chunked : 1; +} winhttp_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; + const char *path; + char *host; + char *port; + char *user_from_url; + char *pass_from_url; + git_cred *cred; + git_cred *url_cred; + int auth_mechanism; + HINTERNET session; + HINTERNET connection; + unsigned use_ssl : 1; +} winhttp_subtransport; + +static int apply_basic_credential(HINTERNET request, git_cred *cred) +{ + git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred; + git_buf buf = GIT_BUF_INIT, raw = GIT_BUF_INIT; + wchar_t *wide = NULL; + int error = -1, wide_len = 0; + + git_buf_printf(&raw, "%s:%s", c->username, c->password); + + if (git_buf_oom(&raw) || + git_buf_puts(&buf, "Authorization: Basic ") < 0 || + git_buf_put_base64(&buf, git_buf_cstr(&raw), raw.size) < 0) + goto on_error; + + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + wide = git__malloc(wide_len * sizeof(wchar_t)); + + if (!wide) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, wide, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + goto on_error; + } + + if (!WinHttpAddRequestHeaders(request, wide, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + + error = 0; + +on_error: + /* We were dealing with plaintext passwords, so clean up after ourselves a bit. */ + if (wide) + memset(wide, 0x0, wide_len * sizeof(wchar_t)); + + if (buf.size) + memset(buf.ptr, 0x0, buf.size); + + if (raw.size) + memset(raw.ptr, 0x0, raw.size); + + git__free(wide); + git_buf_free(&buf); + git_buf_free(&raw); + return error; +} + +static int winhttp_stream_connect(winhttp_stream *s) +{ + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + git_buf buf = GIT_BUF_INIT; + char *proxy_url = NULL; + wchar_t ct[MAX_CONTENT_TYPE_LEN]; + wchar_t *types[] = { L"*/*", NULL }; + BOOL peerdist = FALSE; + int error = -1, wide_len; + + /* Prepare URL */ + git_buf_printf(&buf, "%s%s", t->path, s->service_url); + + if (git_buf_oom(&buf)) + return -1; + + /* Convert URL to wide characters */ + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + s->request_uri = git__malloc(wide_len * sizeof(wchar_t)); + + if (!s->request_uri) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + git_buf_cstr(&buf), -1, s->request_uri, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + goto on_error; + } + + /* Establish request */ + s->request = WinHttpOpenRequest( + t->connection, + s->verb, + s->request_uri, + NULL, + WINHTTP_NO_REFERER, + types, + t->use_ssl ? WINHTTP_FLAG_SECURE : 0); + + if (!s->request) { + giterr_set(GITERR_OS, "Failed to open request"); + goto on_error; + } + + /* Set proxy if necessary */ + if (git_remote__get_http_proxy(t->owner->owner, t->use_ssl, &proxy_url) < 0) + goto on_error; + + if (proxy_url) { + WINHTTP_PROXY_INFO proxy_info; + wchar_t *proxy_wide; + + /* Convert URL to wide characters */ + wide_len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + proxy_url, -1, NULL, 0); + + if (!wide_len) { + giterr_set(GITERR_OS, "Failed to measure string for wide conversion"); + goto on_error; + } + + proxy_wide = git__malloc(wide_len * sizeof(wchar_t)); + + if (!proxy_wide) + goto on_error; + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + proxy_url, -1, proxy_wide, wide_len)) { + giterr_set(GITERR_OS, "Failed to convert string to wide form"); + git__free(proxy_wide); + goto on_error; + } + + /* Strip any trailing forward slash on the proxy URL; + * WinHTTP doesn't like it if one is present */ + if (wide_len > 1 && L'/' == proxy_wide[wide_len - 2]) + proxy_wide[wide_len - 2] = L'\0'; + + proxy_info.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; + proxy_info.lpszProxy = proxy_wide; + proxy_info.lpszProxyBypass = NULL; + + if (!WinHttpSetOption(s->request, + WINHTTP_OPTION_PROXY, + &proxy_info, + sizeof(WINHTTP_PROXY_INFO))) { + giterr_set(GITERR_OS, "Failed to set proxy"); + git__free(proxy_wide); + goto on_error; + } + + git__free(proxy_wide); + } + + /* Strip unwanted headers (X-P2P-PeerDist, X-P2P-PeerDistEx) that WinHTTP + * adds itself. This option may not be supported by the underlying + * platform, so we do not error-check it */ + WinHttpSetOption(s->request, + WINHTTP_OPTION_PEERDIST_EXTENSION_STATE, + &peerdist, + sizeof(peerdist)); + + /* Send Pragma: no-cache header */ + if (!WinHttpAddRequestHeaders(s->request, pragma_nocache, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + + /* Send Content-Type header -- only necessary on a POST */ + if (post_verb == s->verb) { + git_buf_clear(&buf); + if (git_buf_printf(&buf, "Content-Type: application/x-git-%s-request", s->service) < 0) + goto on_error; + + git__utf8_to_16(ct, MAX_CONTENT_TYPE_LEN, git_buf_cstr(&buf)); + + if (!WinHttpAddRequestHeaders(s->request, ct, (ULONG) -1L, WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + goto on_error; + } + } + + /* If requested, disable certificate validation */ + if (t->use_ssl) { + int flags; + + if (t->owner->parent.read_flags(&t->owner->parent, &flags) < 0) + goto on_error; + + if ((GIT_TRANSPORTFLAGS_NO_CHECK_CERT & flags) && + !WinHttpSetOption(s->request, WINHTTP_OPTION_SECURITY_FLAGS, + (LPVOID)&no_check_cert_flags, sizeof(no_check_cert_flags))) { + giterr_set(GITERR_OS, "Failed to set options to ignore cert errors"); + goto on_error; + } + } + + /* If we have a credential on the subtransport, apply it to the request */ + if (t->cred && + t->cred->credtype == GIT_CREDTYPE_USERPASS_PLAINTEXT && + t->auth_mechanism == GIT_WINHTTP_AUTH_BASIC && + apply_basic_credential(s->request, t->cred) < 0) + goto on_error; + + /* If no other credentials have been applied and the URL has username and + * password, use those */ + if (!t->cred && t->user_from_url && t->pass_from_url) { + if (!t->url_cred && + git_cred_userpass_plaintext_new(&t->url_cred, t->user_from_url, t->pass_from_url) < 0) + goto on_error; + if (apply_basic_credential(s->request, t->url_cred) < 0) + goto on_error; + } + + /* We've done everything up to calling WinHttpSendRequest. */ + + error = 0; + +on_error: + git__free(proxy_url); + git_buf_free(&buf); + return error; +} + +static int parse_unauthorized_response( + HINTERNET request, + int *allowed_types, + int *auth_mechanism) +{ + DWORD supported, first, target; + + *allowed_types = 0; + *auth_mechanism = 0; + + /* WinHttpQueryHeaders() must be called before WinHttpQueryAuthSchemes(). + * We can assume this was already done, since we know we are unauthorized. + */ + if (!WinHttpQueryAuthSchemes(request, &supported, &first, &target)) { + giterr_set(GITERR_OS, "Failed to parse supported auth schemes"); + return -1; + } + + if (WINHTTP_AUTH_SCHEME_BASIC & supported) { + *allowed_types |= GIT_CREDTYPE_USERPASS_PLAINTEXT; + *auth_mechanism = GIT_WINHTTP_AUTH_BASIC; + } + + return 0; +} + +static int write_chunk(HINTERNET request, const char *buffer, size_t len) +{ + DWORD bytes_written; + git_buf buf = GIT_BUF_INIT; + + /* Chunk header */ + git_buf_printf(&buf, "%X\r\n", len); + + if (git_buf_oom(&buf)) + return -1; + + if (!WinHttpWriteData(request, + git_buf_cstr(&buf), (DWORD)git_buf_len(&buf), + &bytes_written)) { + git_buf_free(&buf); + giterr_set(GITERR_OS, "Failed to write chunk header"); + return -1; + } + + git_buf_free(&buf); + + /* Chunk body */ + if (!WinHttpWriteData(request, + buffer, (DWORD)len, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write chunk"); + return -1; + } + + /* Chunk footer */ + if (!WinHttpWriteData(request, + "\r\n", 2, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write chunk footer"); + return -1; + } + + return 0; +} + +static int winhttp_stream_read( + git_smart_subtransport_stream *stream, + char *buffer, + size_t buf_size, + size_t *bytes_read) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD dw_bytes_read; + char replay_count = 0; + +replay: + /* Enforce a reasonable cap on the number of replays */ + if (++replay_count >= 7) { + giterr_set(GITERR_NET, "Too many redirects or authentication replays"); + return -1; + } + + /* Connect if necessary */ + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->received_response) { + DWORD status_code, status_code_length, content_type_length, bytes_written; + char expected_content_type_8[MAX_CONTENT_TYPE_LEN]; + wchar_t expected_content_type[MAX_CONTENT_TYPE_LEN], content_type[MAX_CONTENT_TYPE_LEN]; + + if (!s->sent_request) { + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + s->post_body_len, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + } + + if (s->chunked) { + assert(s->verb == post_verb); + + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0 && + write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Write the final chunk. */ + if (!WinHttpWriteData(s->request, + "0\r\n\r\n", 5, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write final chunk"); + return -1; + } + } + else if (s->post_body) { + char *buffer; + DWORD len = s->post_body_len, bytes_read; + + if (INVALID_SET_FILE_POINTER == SetFilePointer(s->post_body, + 0, 0, FILE_BEGIN) && + NO_ERROR != GetLastError()) { + giterr_set(GITERR_OS, "Failed to reset file pointer"); + return -1; + } + + buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + + while (len > 0) { + DWORD bytes_written; + + if (!ReadFile(s->post_body, buffer, + min(CACHED_POST_BODY_BUF_SIZE, len), + &bytes_read, NULL) || + !bytes_read) { + git__free(buffer); + giterr_set(GITERR_OS, "Failed to read from temp file"); + return -1; + } + + if (!WinHttpWriteData(s->request, buffer, + bytes_read, &bytes_written)) { + git__free(buffer); + giterr_set(GITERR_OS, "Failed to write data"); + return -1; + } + + len -= bytes_read; + assert(bytes_read == bytes_written); + } + + git__free(buffer); + + /* Eagerly close the temp file */ + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (!WinHttpReceiveResponse(s->request, 0)) { + giterr_set(GITERR_OS, "Failed to receive response"); + return -1; + } + + /* Verify that we got a 200 back */ + status_code_length = sizeof(status_code); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, + &status_code, &status_code_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to retrieve status code"); + return -1; + } + + /* The implementation of WinHTTP prior to Windows 7 will not + * redirect to an identical URI. Some Git hosters use self-redirects + * as part of their DoS mitigation strategy. Check first to see if we + * have a redirect status code, and that we haven't already streamed + * a post body. (We can't replay a streamed POST.) */ + if (!s->chunked && + (HTTP_STATUS_MOVED == status_code || + HTTP_STATUS_REDIRECT == status_code || + (HTTP_STATUS_REDIRECT_METHOD == status_code && + get_verb == s->verb) || + HTTP_STATUS_REDIRECT_KEEP_VERB == status_code)) { + + /* Check for Windows 7. This workaround is only necessary on + * Windows Vista and earlier. Windows 7 is version 6.1. */ + if (!git_has_win32_version(6, 1)) { + wchar_t *location; + DWORD location_length; + int redirect_cmp; + + /* OK, fetch the Location header from the redirect. */ + if (WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + WINHTTP_NO_OUTPUT_BUFFER, + &location_length, + WINHTTP_NO_HEADER_INDEX) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + giterr_set(GITERR_OS, "Failed to read Location header"); + return -1; + } + + location = git__malloc(location_length); + GITERR_CHECK_ALLOC(location); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_LOCATION, + WINHTTP_HEADER_NAME_BY_INDEX, + location, + &location_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to read Location header"); + git__free(location); + return -1; + } + + /* Compare the Location header with the request URI */ + redirect_cmp = wcscmp(location, s->request_uri); + git__free(location); + + if (!redirect_cmp) { + /* Replay the request */ + WinHttpCloseHandle(s->request); + s->request = NULL; + s->sent_request = 0; + + goto replay; + } + } + } + + /* Handle authentication failures */ + if (HTTP_STATUS_DENIED == status_code && + get_verb == s->verb && t->owner->cred_acquire_cb) { + int allowed_types; + + if (parse_unauthorized_response(s->request, &allowed_types, &t->auth_mechanism) < 0) + return -1; + + if (allowed_types && + (!t->cred || 0 == (t->cred->credtype & allowed_types))) { + + if (t->owner->cred_acquire_cb(&t->cred, t->owner->url, t->user_from_url, allowed_types, t->owner->cred_acquire_payload) < 0) + return -1; + + assert(t->cred); + + WinHttpCloseHandle(s->request); + s->request = NULL; + s->sent_request = 0; + + /* Successfully acquired a credential */ + goto replay; + } + } + + if (HTTP_STATUS_OK != status_code) { + giterr_set(GITERR_NET, "Request failed with status code: %d", status_code); + return -1; + } + + /* Verify that we got the correct content-type back */ + if (post_verb == s->verb) + snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-result", s->service); + else + snprintf(expected_content_type_8, MAX_CONTENT_TYPE_LEN, "application/x-git-%s-advertisement", s->service); + + git__utf8_to_16(expected_content_type, MAX_CONTENT_TYPE_LEN, expected_content_type_8); + content_type_length = sizeof(content_type); + + if (!WinHttpQueryHeaders(s->request, + WINHTTP_QUERY_CONTENT_TYPE, + WINHTTP_HEADER_NAME_BY_INDEX, + &content_type, &content_type_length, + WINHTTP_NO_HEADER_INDEX)) { + giterr_set(GITERR_OS, "Failed to retrieve response content-type"); + return -1; + } + + if (wcscmp(expected_content_type, content_type)) { + giterr_set(GITERR_NET, "Received unexpected content-type"); + return -1; + } + + s->received_response = 1; + } + + if (!WinHttpReadData(s->request, + (LPVOID)buffer, + (DWORD)buf_size, + &dw_bytes_read)) + { + giterr_set(GITERR_OS, "Failed to read data"); + return -1; + } + + *bytes_read = dw_bytes_read; + + return 0; +} + +static int winhttp_stream_write_single( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD bytes_written; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* This implementation of write permits only a single call. */ + if (s->sent_request) { + giterr_set(GITERR_NET, "Subtransport configured for only one write"); + return -1; + } + + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + (DWORD)len, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + + if (!WinHttpWriteData(s->request, + (LPCVOID)buffer, + (DWORD)len, + &bytes_written)) { + giterr_set(GITERR_OS, "Failed to write data"); + return -1; + } + + assert((DWORD)len == bytes_written); + + return 0; +} + +static int put_uuid_string(LPWSTR buffer, size_t buffer_len_cch) +{ + UUID uuid; + RPC_STATUS status = UuidCreate(&uuid); + HRESULT result; + + if (RPC_S_OK != status && + RPC_S_UUID_LOCAL_ONLY != status && + RPC_S_UUID_NO_ADDRESS != status) { + giterr_set(GITERR_NET, "Unable to generate name for temp file"); + return -1; + } + + if (buffer_len_cch < UUID_LENGTH_CCH + 1) { + giterr_set(GITERR_NET, "Buffer too small for name of temp file"); + return -1; + } + + result = StringCbPrintfW( + buffer, buffer_len_cch, + L"%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x", + uuid.Data1, uuid.Data2, uuid.Data3, + uuid.Data4[0], uuid.Data4[1], uuid.Data4[2], uuid.Data4[3], + uuid.Data4[4], uuid.Data4[5], uuid.Data4[6], uuid.Data4[7]); + + if (FAILED(result)) { + giterr_set(GITERR_OS, "Unable to generate name for temp file"); + return -1; + } + + return 0; +} + +static int get_temp_file(LPWSTR buffer, DWORD buffer_len_cch) +{ + size_t len; + + if (!GetTempPathW(buffer_len_cch, buffer)) { + giterr_set(GITERR_OS, "Failed to get temp path"); + return -1; + } + + len = wcslen(buffer); + + if (buffer[len - 1] != '\\' && len < buffer_len_cch) + buffer[len++] = '\\'; + + if (put_uuid_string(&buffer[len], (size_t)buffer_len_cch - len) < 0) + return -1; + + return 0; +} + +static int winhttp_stream_write_buffered( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + DWORD bytes_written; + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + /* Buffer the payload, using a temporary file so we delegate + * memory management of the data to the operating system. */ + if (!s->post_body) { + wchar_t temp_path[MAX_PATH + 1]; + + if (get_temp_file(temp_path, MAX_PATH + 1) < 0) + return -1; + + s->post_body = CreateFileW(temp_path, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_DELETE, NULL, + CREATE_NEW, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE | FILE_FLAG_SEQUENTIAL_SCAN, + NULL); + + if (INVALID_HANDLE_VALUE == s->post_body) { + s->post_body = NULL; + giterr_set(GITERR_OS, "Failed to create temporary file"); + return -1; + } + } + + if (!WriteFile(s->post_body, buffer, (DWORD)len, &bytes_written, NULL)) { + giterr_set(GITERR_OS, "Failed to write to temporary file"); + return -1; + } + + assert((DWORD)len == bytes_written); + + s->post_body_len += bytes_written; + + return 0; +} + +static int winhttp_stream_write_chunked( + git_smart_subtransport_stream *stream, + const char *buffer, + size_t len) +{ + winhttp_stream *s = (winhttp_stream *)stream; + winhttp_subtransport *t = OWNING_SUBTRANSPORT(s); + + if (!s->request && winhttp_stream_connect(s) < 0) + return -1; + + if (!s->sent_request) { + /* Send Transfer-Encoding: chunked header */ + if (!WinHttpAddRequestHeaders(s->request, + transfer_encoding, (ULONG) -1L, + WINHTTP_ADDREQ_FLAG_ADD)) { + giterr_set(GITERR_OS, "Failed to add a header to the request"); + return -1; + } + + if (!WinHttpSendRequest(s->request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, + WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0)) { + giterr_set(GITERR_OS, "Failed to send request"); + return -1; + } + + s->sent_request = 1; + } + + if (len > CACHED_POST_BODY_BUF_SIZE) { + /* Flush, if necessary */ + if (s->chunk_buffer_len > 0) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + } + + /* Write chunk directly */ + if (write_chunk(s->request, buffer, len) < 0) + return -1; + } + else { + /* Append as much to the buffer as we can */ + int count = min(CACHED_POST_BODY_BUF_SIZE - s->chunk_buffer_len, (int)len); + + if (!s->chunk_buffer) + s->chunk_buffer = git__malloc(CACHED_POST_BODY_BUF_SIZE); + + memcpy(s->chunk_buffer + s->chunk_buffer_len, buffer, count); + s->chunk_buffer_len += count; + buffer += count; + len -= count; + + /* Is the buffer full? If so, then flush */ + if (CACHED_POST_BODY_BUF_SIZE == s->chunk_buffer_len) { + if (write_chunk(s->request, s->chunk_buffer, s->chunk_buffer_len) < 0) + return -1; + + s->chunk_buffer_len = 0; + + /* Is there any remaining data from the source? */ + if (len > 0) { + memcpy(s->chunk_buffer, buffer, len); + s->chunk_buffer_len = (unsigned int)len; + } + } + } + + return 0; +} + +static void winhttp_stream_free(git_smart_subtransport_stream *stream) +{ + winhttp_stream *s = (winhttp_stream *)stream; + + if (s->chunk_buffer) { + git__free(s->chunk_buffer); + s->chunk_buffer = NULL; + } + + if (s->post_body) { + CloseHandle(s->post_body); + s->post_body = NULL; + } + + if (s->request_uri) { + git__free(s->request_uri); + s->request_uri = NULL; + } + + if (s->request) { + WinHttpCloseHandle(s->request); + s->request = NULL; + } + + git__free(s); +} + +static int winhttp_stream_alloc(winhttp_subtransport *t, winhttp_stream **stream) +{ + winhttp_stream *s; + + if (!stream) + return -1; + + s = git__calloc(sizeof(winhttp_stream), 1); + GITERR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = winhttp_stream_read; + s->parent.write = winhttp_stream_write_single; + s->parent.free = winhttp_stream_free; + + *stream = s; + + return 0; +} + +static int winhttp_connect( + winhttp_subtransport *t, + const char *url) +{ + wchar_t *ua = L"git/1.0 (libgit2 " WIDEN(LIBGIT2_VERSION) L")"; + wchar_t host[GIT_WIN_PATH]; + int32_t port; + const char *default_port; + int ret; + + if (!git__prefixcmp(url, prefix_http)) { + url = url + strlen(prefix_http); + default_port = "80"; + } + + if (!git__prefixcmp(url, prefix_https)) { + url += strlen(prefix_https); + default_port = "443"; + t->use_ssl = 1; + } + + if ((ret = gitno_extract_url_parts(&t->host, &t->port, &t->user_from_url, + &t->pass_from_url, url, default_port)) < 0) + return ret; + + t->path = strchr(url, '/'); + + /* Prepare port */ + if (git__strtol32(&port, t->port, NULL, 10) < 0) + return -1; + + /* Prepare host */ + git__utf8_to_16(host, GIT_WIN_PATH, t->host); + + /* Establish session */ + t->session = WinHttpOpen( + ua, + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0); + + if (!t->session) { + giterr_set(GITERR_OS, "Failed to init WinHTTP"); + return -1; + } + + /* Establish connection */ + t->connection = WinHttpConnect( + t->session, + host, + port, + 0); + + if (!t->connection) { + giterr_set(GITERR_OS, "Failed to connect to host"); + return -1; + } + + return 0; +} + +static int winhttp_uploadpack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = upload_pack_service; + s->service_url = upload_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_uploadpack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = upload_pack_service; + s->service_url = upload_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_receivepack_ls( + winhttp_subtransport *t, + winhttp_stream *s) +{ + s->service = receive_pack_service; + s->service_url = receive_pack_ls_service_url; + s->verb = get_verb; + + return 0; +} + +static int winhttp_receivepack( + winhttp_subtransport *t, + winhttp_stream *s) +{ + /* WinHTTP only supports Transfer-Encoding: chunked + * on Windows Vista (NT 6.0) and higher. */ + s->chunked = git_has_win32_version(6, 0); + + if (s->chunked) + s->parent.write = winhttp_stream_write_chunked; + else + s->parent.write = winhttp_stream_write_buffered; + + s->service = receive_pack_service; + s->service_url = receive_pack_service_url; + s->verb = post_verb; + + return 0; +} + +static int winhttp_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + winhttp_stream *s; + int ret = -1; + + if (!t->connection && + winhttp_connect(t, url) < 0) + return -1; + + if (winhttp_stream_alloc(t, &s) < 0) + return -1; + + if (!stream) + return -1; + + switch (action) + { + case GIT_SERVICE_UPLOADPACK_LS: + ret = winhttp_uploadpack_ls(t, s); + break; + + case GIT_SERVICE_UPLOADPACK: + ret = winhttp_uploadpack(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK_LS: + ret = winhttp_receivepack_ls(t, s); + break; + + case GIT_SERVICE_RECEIVEPACK: + ret = winhttp_receivepack(t, s); + break; + + default: + assert(0); + } + + if (!ret) + *stream = &s->parent; + + return ret; +} + +static int winhttp_close(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + int ret = 0; + + if (t->host) { + git__free(t->host); + t->host = NULL; + } + + if (t->port) { + git__free(t->port); + t->port = NULL; + } + + if (t->user_from_url) { + git__free(t->user_from_url); + t->user_from_url = NULL; + } + + if (t->pass_from_url) { + git__free(t->pass_from_url); + t->pass_from_url = NULL; + } + + if (t->cred) { + t->cred->free(t->cred); + t->cred = NULL; + } + + if (t->url_cred) { + t->url_cred->free(t->url_cred); + t->url_cred = NULL; + } + + if (t->connection) { + if (!WinHttpCloseHandle(t->connection)) { + giterr_set(GITERR_OS, "Unable to close connection"); + ret = -1; + } + + t->connection = NULL; + } + + if (t->session) { + if (!WinHttpCloseHandle(t->session)) { + giterr_set(GITERR_OS, "Unable to close session"); + ret = -1; + } + + t->session = NULL; + } + + return ret; +} + +static void winhttp_free(git_smart_subtransport *subtransport) +{ + winhttp_subtransport *t = (winhttp_subtransport *)subtransport; + + winhttp_close(subtransport); + + git__free(t); +} + +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner) +{ + winhttp_subtransport *t; + + if (!out) + return -1; + + t = git__calloc(sizeof(winhttp_subtransport), 1); + GITERR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = winhttp_action; + t->parent.close = winhttp_close; + t->parent.free = winhttp_free; + + *out = (git_smart_subtransport *) t; + return 0; +} + +#endif /* GIT_WINHTTP */ |