/* * Copyright (C) 2009-2012 the libgit2 contributors * * 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 #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" enum last_cb { NONE, FIELD, VALUE }; typedef struct { git_transport parent; http_parser_settings settings; git_buf buf; 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; char *path; char *host; char *port; char *service; char buffer[65536]; #ifdef GIT_WIN32 WSADATA wsd; #endif } transport_http; static int gen_request(git_buf *buf, const char *path, const char *host, const char *op, const char *service, ssize_t content_length, int ls) { if (path == NULL) /* Is 'git fetch http://host.com/' valid? */ path = "/"; 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_puts(buf, "Accept: */*\r\n"); } git_buf_puts(buf, "\r\n"); if (git_buf_oom(buf)) return -1; return 0; } static int do_connect(transport_http *t, const char *host, const char *port) { if (t->parent.connected && http_should_keep_alive(&t->parser)) return 0; if (gitno_connect((git_transport *) t, host, port) < 0) return -1; t->parent.connected = 1; 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); } if (t->ct_found) { t->last_cb = FIELD; return 0; } if (t->last_cb != FIELD) git_buf_clear(buf); git_buf_put(buf, str, len); t->last_cb = FIELD; return git_buf_oom(buf); } 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; if (t->ct_finished) { t->last_cb = VALUE; return 0; } if (t->last_cb == VALUE) git_buf_put(buf, str, len); 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); } t->last_cb = VALUE; return git_buf_oom(buf); } static int on_headers_complete(http_parser *parser) { transport_http *t = (transport_http *) parser->data; git_buf *buf = &t->buf; /* The content-type is text/plain for 404, so don't validate */ if (parser->status_code == 404) { git_buf_clear(buf); return 0; } if (t->content_type == NULL) { t->content_type = git__strdup(git_buf_cstr(buf)); if (t->content_type == NULL) return t->error = -1; } git_buf_clear(buf); git_buf_printf(buf, "application/x-git-%s-advertisement", t->service); if (git_buf_oom(buf)) return t->error = -1; if (strcmp(t->content_type, git_buf_cstr(buf))) return t->error = -1; git_buf_clear(buf); return 0; } static int on_message_complete(http_parser *parser) { transport_http *t = (transport_http *) parser->data; t->transfer_finished = 1; if (parser->status_code == 404) { giterr_set(GITERR_NET, "Remote error: %s", git_buf_cstr(&t->buf)); t->error = -1; } return 0; } static int on_body_fill_buffer(http_parser *parser, const char *str, size_t len) { git_transport *transport = (git_transport *) parser->data; transport_http *t = (transport_http *) parser->data; gitno_buffer *buf = &transport->buffer; if (buf->len - buf->offset < len) { giterr_set(GITERR_NET, "Can't fit data in the buffer"); return t->error = -1; } memcpy(buf->data + buf->offset, str, len); buf->offset += len; return 0; } static int http_recv_cb(gitno_buffer *buf) { git_transport *transport = (git_transport *) buf->cb_data; transport_http *t = (transport_http *) transport; size_t old_len; gitno_buffer inner; char buffer[2048]; int error; if (t->transfer_finished) return 0; gitno_buffer_setup(transport, &inner, buffer, sizeof(buffer)); if ((error = gitno_recv(&inner)) < 0) return -1; old_len = buf->offset; http_parser_execute(&t->parser, &t->settings, inner.data, inner.offset); if (t->error < 0) return t->error; return (int)(buf->offset - old_len); } /* Set up the gitno_buffer so calling gitno_recv() grabs data from the HTTP response */ static void setup_gitno_buffer(git_transport *transport) { transport_http *t = (transport_http *) transport; http_parser_init(&t->parser, HTTP_RESPONSE); t->parser.data = t; t->transfer_finished = 0; memset(&t->settings, 0x0, sizeof(http_parser_settings)); 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; gitno_buffer_setup_callback(transport, &transport->buffer, t->buffer, sizeof(t->buffer), http_recv_cb, t); } static int http_connect(git_transport *transport, int direction) { 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 = "http://", *prefix_https = "https://"; const char *default_port; git_pkt *pkt; if (direction == GIT_DIR_PUSH) { giterr_set(GITERR_NET, "Pushing over HTTP is not implemented"); return -1; } t->parent.direction = direction; if (!git__prefixcmp(url, prefix_http)) { url = t->parent.url + strlen(prefix_http); default_port = "80"; } if (!git__prefixcmp(url, prefix_https)) { url += strlen(prefix_https); default_port = "443"; } t->path = strchr(url, '/'); if ((ret = gitno_extract_host_and_port(&t->host, &t->port, url, default_port)) < 0) goto cleanup; t->service = git__strdup(service); GITERR_CHECK_ALLOC(t->service); if ((ret = do_connect(t, t->host, t->port)) < 0) goto cleanup; /* Generate and send the HTTP request */ if ((ret = gen_request(&request, t->path, t->host, "GET", service, 0, 1)) < 0) { giterr_set(GITERR_NET, "Failed to generate request"); goto cleanup; } if (gitno_send(transport, request.ptr, request.size, 0) < 0) goto cleanup; setup_gitno_buffer(transport); if ((ret = git_protocol_store_refs(transport, 2)) < 0) goto cleanup; pkt = git_vector_get(&transport->refs, 0); if (pkt == NULL || pkt->type != GIT_PKT_COMMENT) { giterr_set(GITERR_NET, "Invalid HTTP response"); return t->error = -1; } else { /* Remove the comment and flush pkts */ git_vector_remove(&transport->refs, 0); git__free(pkt); pkt = git_vector_get(&transport->refs, 0); git_vector_remove(&transport->refs, 0); git__free(pkt); } if (git_protocol_detect_caps(git_vector_get(&transport->refs, 0), &transport->caps) < 0) return t->error = -1; cleanup: git_buf_free(&request); git_buf_clear(&t->buf); return ret; } static int http_negotiation_step(struct git_transport *transport, void *data, size_t len) { transport_http *t = (transport_http *) transport; git_buf request = GIT_BUF_INIT; int ret; /* First, send the data as a HTTP POST request */ if ((ret = do_connect(t, t->host, t->port)) < 0) return -1; if ((ret = gen_request(&request, t->path, t->host, "POST", "upload-pack", len, 0)) < 0) goto on_error; if ((ret = gitno_send(transport, request.ptr, request.size, 0)) < 0) goto on_error; if ((ret = gitno_send(transport, data, len, 0)) < 0) goto on_error; git_buf_free(&request); /* Then we need to set up the buffer to grab data from the HTTP response */ setup_gitno_buffer(transport); return 0; on_error: git_buf_free(&request); return -1; } static int http_close(git_transport *transport) { if (gitno_ssl_teardown(transport) < 0) return -1; if (gitno_close(transport->socket) < 0) { giterr_set(GITERR_OS, "Failed to close the socket: %s", strerror(errno)); return -1; } transport->connected = 0; return 0; } static void http_free(git_transport *transport) { transport_http *t = (transport_http *) transport; git_vector *refs = &transport->refs; git_vector *common = &transport->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); } git_vector_free(refs); git_vector_foreach(common, i, p) { git_pkt_free(p); } git_vector_free(common); git_buf_free(&t->buf); git__free(t->content_type); git__free(t->host); git__free(t->port); git__free(t->service); git__free(t->parent.url); git__free(t); } int git_transport_http(git_transport **out) { transport_http *t; t = git__malloc(sizeof(transport_http)); GITERR_CHECK_ALLOC(t); memset(t, 0x0, sizeof(transport_http)); t->parent.connect = http_connect; t->parent.negotiation_step = http_negotiation_step; t->parent.close = http_close; t->parent.free = http_free; t->parent.rpc = 1; if (git_vector_init(&t->parent.refs, 16, NULL) < 0) { git__free(t); return -1; } #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; return 0; } int git_transport_https(git_transport **out) { #ifdef GIT_SSL transport_http *t; if (git_transport_http((git_transport **)&t) < 0) return -1; t->parent.use_ssl = 1; t->parent.check_cert = 1; *out = (git_transport *) t; return 0; #else GIT_UNUSED(out); giterr_set(GITERR_NET, "HTTPS support not available"); return -1; #endif }