diff options
Diffstat (limited to 'src/transports/http.c')
-rw-r--r-- | src/transports/http.c | 1203 |
1 files changed, 737 insertions, 466 deletions
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 */ |