diff options
Diffstat (limited to 'http.c')
-rw-r--r-- | http.c | 739 |
1 files changed, 627 insertions, 112 deletions
@@ -1,9 +1,15 @@ +#include "git-compat-util.h" #include "http.h" #include "pack.h" #include "sideband.h" #include "run-command.h" #include "url.h" +#include "urlmatch.h" #include "credential.h" +#include "version.h" +#include "pkt-line.h" +#include "gettext.h" +#include "transport.h" int active_requests; int http_is_verbose; @@ -29,7 +35,23 @@ static CURL *curl_default; char curl_errorstr[CURL_ERROR_SIZE]; static int curl_ssl_verify = -1; +static int curl_ssl_try; static const char *ssl_cert; +static const char *ssl_cipherlist; +static const char *ssl_version; +static struct { + const char *name; + long ssl_version; +} sslversions[] = { + { "sslv2", CURL_SSLVERSION_SSLv2 }, + { "sslv3", CURL_SSLVERSION_SSLv3 }, + { "tlsv1", CURL_SSLVERSION_TLSv1 }, +#if LIBCURL_VERSION_NUM >= 0x072200 + { "tlsv1.0", CURL_SSLVERSION_TLSv1_0 }, + { "tlsv1.1", CURL_SSLVERSION_TLSv1_1 }, + { "tlsv1.2", CURL_SSLVERSION_TLSv1_2 }, +#endif +}; #if LIBCURL_VERSION_NUM >= 0x070903 static const char *ssl_key; #endif @@ -42,7 +64,8 @@ static long curl_low_speed_time = -1; static int curl_ftp_no_epsv; static const char *curl_http_proxy; static const char *curl_cookie_file; -static struct credential http_auth = CREDENTIAL_INIT; +static int curl_save_cookies; +struct credential http_auth = CREDENTIAL_INIT; static int http_proactive_auth; static const char *user_agent; @@ -56,12 +79,17 @@ static const char *user_agent; static struct credential cert_auth = CREDENTIAL_INIT; static int ssl_cert_password_required; +#ifdef LIBCURL_CAN_HANDLE_AUTH_ANY +static unsigned long http_auth_methods = CURLAUTH_ANY; +#endif static struct curl_slist *pragma_header; static struct curl_slist *no_pragma_header; static struct active_request_slot *active_queue_head; +static char *cached_accept_language; + size_t fread_buffer(char *ptr, size_t eltsize, size_t nmemb, void *buffer_) { size_t size = eltsize * nmemb; @@ -108,6 +136,37 @@ size_t fwrite_null(char *ptr, size_t eltsize, size_t nmemb, void *strbuf) return eltsize * nmemb; } +static void closedown_active_slot(struct active_request_slot *slot) +{ + active_requests--; + slot->in_use = 0; +} + +static void finish_active_slot(struct active_request_slot *slot) +{ + closedown_active_slot(slot); + curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CODE, &slot->http_code); + + if (slot->finished != NULL) + (*slot->finished) = 1; + + /* Store slot results so they can be read after the slot is reused */ + if (slot->results != NULL) { + slot->results->curl_result = slot->curl_result; + slot->results->http_code = slot->http_code; +#if LIBCURL_VERSION_NUM >= 0x070a08 + curl_easy_getinfo(slot->curl, CURLINFO_HTTPAUTH_AVAIL, + &slot->results->auth_avail); +#else + slot->results->auth_avail = 0; +#endif + } + + /* Run callback if appropriate */ + if (slot->callback_func != NULL) + slot->callback_func(slot->callback_data); +} + #ifdef USE_CURL_MULTI static void process_curl_messages(void) { @@ -144,6 +203,10 @@ static int http_options(const char *var, const char *value, void *cb) curl_ssl_verify = git_config_bool(var, value); return 0; } + if (!strcmp("http.sslcipherlist", var)) + return git_config_string(&ssl_cipherlist, var, value); + if (!strcmp("http.sslversion", var)) + return git_config_string(&ssl_version, var, value); if (!strcmp("http.sslcert", var)) return git_config_string(&ssl_cert, var, value); #if LIBCURL_VERSION_NUM >= 0x070903 @@ -157,8 +220,11 @@ static int http_options(const char *var, const char *value, void *cb) if (!strcmp("http.sslcainfo", var)) return git_config_string(&ssl_cainfo, var, value); if (!strcmp("http.sslcertpasswordprotected", var)) { - if (git_config_bool(var, value)) - ssl_cert_password_required = 1; + ssl_cert_password_required = git_config_bool(var, value); + return 0; + } + if (!strcmp("http.ssltry", var)) { + curl_ssl_try = git_config_bool(var, value); return 0; } if (!strcmp("http.minsessions", var)) { @@ -193,6 +259,10 @@ static int http_options(const char *var, const char *value, void *cb) if (!strcmp("http.cookiefile", var)) return git_config_string(&curl_cookie_file, var, value); + if (!strcmp("http.savecookies", var)) { + curl_save_cookies = git_config_bool(var, value); + return 0; + } if (!strcmp("http.postbuffer", var)) { http_post_buffer = git_config_int(var, value); @@ -210,14 +280,29 @@ static int http_options(const char *var, const char *value, void *cb) static void init_curl_http_auth(CURL *result) { - if (http_auth.username) { - struct strbuf up = STRBUF_INIT; - credential_fill(&http_auth); - strbuf_addf(&up, "%s:%s", - http_auth.username, http_auth.password); - curl_easy_setopt(result, CURLOPT_USERPWD, - strbuf_detach(&up, NULL)); + if (!http_auth.username) + return; + + credential_fill(&http_auth); + +#if LIBCURL_VERSION_NUM >= 0x071301 + curl_easy_setopt(result, CURLOPT_USERNAME, http_auth.username); + curl_easy_setopt(result, CURLOPT_PASSWORD, http_auth.password); +#else + { + static struct strbuf up = STRBUF_INIT; + /* + * Note that we assume we only ever have a single set of + * credentials in a given program run, so we do not have + * to worry about updating this buffer, only setting its + * initial value. + */ + if (!up.len) + strbuf_addf(&up, "%s:%s", + http_auth.username, http_auth.password); + curl_easy_setopt(result, CURLOPT_USERPWD, up.buf); } +#endif } static int has_cert_password(void) @@ -226,15 +311,56 @@ static int has_cert_password(void) return 0; if (!cert_auth.password) { cert_auth.protocol = xstrdup("cert"); + cert_auth.username = xstrdup(""); cert_auth.path = xstrdup(ssl_cert); credential_fill(&cert_auth); } return 1; } +#if LIBCURL_VERSION_NUM >= 0x071900 +static void set_curl_keepalive(CURL *c) +{ + curl_easy_setopt(c, CURLOPT_TCP_KEEPALIVE, 1); +} + +#elif LIBCURL_VERSION_NUM >= 0x071000 +static int sockopt_callback(void *client, curl_socket_t fd, curlsocktype type) +{ + int ka = 1; + int rc; + socklen_t len = (socklen_t)sizeof(ka); + + if (type != CURLSOCKTYPE_IPCXN) + return 0; + + rc = setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&ka, len); + if (rc < 0) + warning("unable to set SO_KEEPALIVE on socket %s", + strerror(errno)); + + return 0; /* CURL_SOCKOPT_OK only exists since curl 7.21.5 */ +} + +static void set_curl_keepalive(CURL *c) +{ + curl_easy_setopt(c, CURLOPT_SOCKOPTFUNCTION, sockopt_callback); +} + +#else +static void set_curl_keepalive(CURL *c) +{ + /* not supported on older curl versions */ +} +#endif + static CURL *get_curl_handle(void) { CURL *result = curl_easy_init(); + long allowed_protocols = 0; + + if (!result) + die("curl_easy_init failed"); if (!curl_ssl_verify) { curl_easy_setopt(result, CURLOPT_SSL_VERIFYPEER, 0); @@ -256,6 +382,28 @@ static CURL *get_curl_handle(void) if (http_proactive_auth) init_curl_http_auth(result); + if (getenv("GIT_SSL_VERSION")) + ssl_version = getenv("GIT_SSL_VERSION"); + if (ssl_version && *ssl_version) { + int i; + for (i = 0; i < ARRAY_SIZE(sslversions); i++) { + if (!strcmp(ssl_version, sslversions[i].name)) { + curl_easy_setopt(result, CURLOPT_SSLVERSION, + sslversions[i].ssl_version); + break; + } + } + if (i == ARRAY_SIZE(sslversions)) + warning("unsupported ssl version %s: using default", + ssl_version); + } + + if (getenv("GIT_SSL_CIPHER_LIST")) + ssl_cipherlist = getenv("GIT_SSL_CIPHER_LIST"); + if (ssl_cipherlist != NULL && *ssl_cipherlist) + curl_easy_setopt(result, CURLOPT_SSL_CIPHER_LIST, + ssl_cipherlist); + if (ssl_cert != NULL) curl_easy_setopt(result, CURLOPT_SSLCERT, ssl_cert); if (has_cert_password()) @@ -270,7 +418,6 @@ static CURL *get_curl_handle(void) #endif if (ssl_cainfo != NULL) curl_easy_setopt(result, CURLOPT_CAINFO, ssl_cainfo); - curl_easy_setopt(result, CURLOPT_FAILONERROR, 1); if (curl_low_speed_limit > 0 && curl_low_speed_time > 0) { curl_easy_setopt(result, CURLOPT_LOW_SPEED_LIMIT, @@ -280,23 +427,50 @@ static CURL *get_curl_handle(void) } curl_easy_setopt(result, CURLOPT_FOLLOWLOCATION, 1); + curl_easy_setopt(result, CURLOPT_MAXREDIRS, 20); #if LIBCURL_VERSION_NUM >= 0x071301 curl_easy_setopt(result, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); #elif LIBCURL_VERSION_NUM >= 0x071101 curl_easy_setopt(result, CURLOPT_POST301, 1); #endif +#if LIBCURL_VERSION_NUM >= 0x071304 + if (is_transport_allowed("http")) + allowed_protocols |= CURLPROTO_HTTP; + if (is_transport_allowed("https")) + allowed_protocols |= CURLPROTO_HTTPS; + if (is_transport_allowed("ftp")) + allowed_protocols |= CURLPROTO_FTP; + if (is_transport_allowed("ftps")) + allowed_protocols |= CURLPROTO_FTPS; + curl_easy_setopt(result, CURLOPT_REDIR_PROTOCOLS, allowed_protocols); +#else + if (transport_restrict_protocols()) + warning("protocol restrictions not applied to curl redirects because\n" + "your curl version is too old (>= 7.19.4)"); +#endif if (getenv("GIT_CURL_VERBOSE")) curl_easy_setopt(result, CURLOPT_VERBOSE, 1); curl_easy_setopt(result, CURLOPT_USERAGENT, - user_agent ? user_agent : GIT_HTTP_USER_AGENT); + user_agent ? user_agent : git_user_agent()); if (curl_ftp_no_epsv) curl_easy_setopt(result, CURLOPT_FTP_USE_EPSV, 0); - if (curl_http_proxy) +#ifdef CURLOPT_USE_SSL + if (curl_ssl_try) + curl_easy_setopt(result, CURLOPT_USE_SSL, CURLUSESSL_TRY); +#endif + + if (curl_http_proxy) { curl_easy_setopt(result, CURLOPT_PROXY, curl_http_proxy); + } +#if LIBCURL_VERSION_NUM >= 0x070a07 + curl_easy_setopt(result, CURLOPT_PROXYAUTH, CURLAUTH_ANY); +#endif + + set_curl_keepalive(result); return result; } @@ -312,12 +486,23 @@ void http_init(struct remote *remote, const char *url, int proactive_auth) { char *low_speed_limit; char *low_speed_time; + char *normalized_url; + struct urlmatch_config config = { STRING_LIST_INIT_DUP }; + + config.section = "http"; + config.key = NULL; + config.collect_fn = http_options; + config.cascade_fn = git_default_config; + config.cb = NULL; http_is_verbose = 0; + normalized_url = url_normalize(url, &config.url); - git_config(http_options, NULL); + git_config(urlmatch_config_entry, &config); + free(normalized_url); - curl_global_init(CURL_GLOBAL_ALL); + if (curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) + die("curl_global_init failed"); http_proactive_auth = proactive_auth; @@ -335,10 +520,8 @@ void http_init(struct remote *remote, const char *url, int proactive_auth) } curlm = curl_multi_init(); - if (curlm == NULL) { - fprintf(stderr, "Error creating curl multi handle.\n"); - exit(1); - } + if (!curlm) + die("curl_multi_init failed"); #endif if (getenv("GIT_SSL_NO_VERIFY")) @@ -378,7 +561,7 @@ void http_init(struct remote *remote, const char *url, int proactive_auth) credential_from_url(&http_auth, url); if (!ssl_cert_password_required && getenv("GIT_SSL_CERT_PASSWORD_PROTECTED") && - !prefixcmp(url, "https://")) + starts_with(url, "https://")) ssl_cert_password_required = 1; } @@ -430,6 +613,9 @@ void http_cleanup(void) cert_auth.password = NULL; } ssl_cert_password_required = 0; + + free(cached_accept_language); + cached_accept_language = NULL; } struct active_request_slot *get_active_slot(void) @@ -484,6 +670,8 @@ struct active_request_slot *get_active_slot(void) slot->callback_data = NULL; slot->callback_func = NULL; curl_easy_setopt(slot->curl, CURLOPT_COOKIEFILE, curl_cookie_file); + if (curl_save_cookies) + curl_easy_setopt(slot->curl, CURLOPT_COOKIEJAR, curl_cookie_file); curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, pragma_header); curl_easy_setopt(slot->curl, CURLOPT_ERRORBUFFER, curl_errorstr); curl_easy_setopt(slot->curl, CURLOPT_CUSTOMREQUEST, NULL); @@ -492,6 +680,12 @@ struct active_request_slot *get_active_slot(void) curl_easy_setopt(slot->curl, CURLOPT_POSTFIELDS, NULL); curl_easy_setopt(slot->curl, CURLOPT_UPLOAD, 0); curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); + curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, 1); +#ifdef LIBCURL_CAN_HANDLE_AUTH_ANY + curl_easy_setopt(slot->curl, CURLOPT_HTTPAUTH, http_auth_methods); +#endif + if (http_auth.password) + init_curl_http_auth(slot->curl); return slot; } @@ -617,6 +811,18 @@ void run_active_slot(struct active_request_slot *slot) FD_ZERO(&excfds); curl_multi_fdset(curlm, &readfds, &writefds, &excfds, &max_fd); + /* + * It can happen that curl_multi_timeout returns a pathologically + * long timeout when curl_multi_fdset returns no file descriptors + * to read. See commit message for more details. + */ + if (max_fd < 0 && + (select_timeout.tv_sec > 0 || + select_timeout.tv_usec > 50000)) { + select_timeout.tv_sec = 0; + select_timeout.tv_usec = 50000; + } + select(max_fd+1, &readfds, &writefds, &excfds, &select_timeout); } } @@ -628,12 +834,6 @@ void run_active_slot(struct active_request_slot *slot) #endif } -static void closedown_active_slot(struct active_request_slot *slot) -{ - active_requests--; - slot->in_use = 0; -} - static void release_active_slot(struct active_request_slot *slot) { closedown_active_slot(slot); @@ -650,25 +850,6 @@ static void release_active_slot(struct active_request_slot *slot) #endif } -void finish_active_slot(struct active_request_slot *slot) -{ - closedown_active_slot(slot); - curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CODE, &slot->http_code); - - if (slot->finished != NULL) - (*slot->finished) = 1; - - /* Store slot results so they can be read after the slot is reused */ - if (slot->results != NULL) { - slot->results->curl_result = slot->curl_result; - slot->results->http_code = slot->http_code; - } - - /* Run callback if appropriate */ - if (slot->callback_func != NULL) - slot->callback_func(slot->callback_data); -} - void finish_all_active_slots(void) { struct active_request_slot *slot = active_queue_head; @@ -731,20 +912,283 @@ char *get_remote_object_url(const char *url, const char *hex, return strbuf_detach(&buf, NULL); } +static int handle_curl_result(struct slot_results *results) +{ + /* + * If we see a failing http code with CURLE_OK, we have turned off + * FAILONERROR (to keep the server's custom error response), and should + * translate the code into failure here. + */ + if (results->curl_result == CURLE_OK && + results->http_code >= 400) { + results->curl_result = CURLE_HTTP_RETURNED_ERROR; + /* + * Normally curl will already have put the "reason phrase" + * from the server into curl_errorstr; unfortunately without + * FAILONERROR it is lost, so we can give only the numeric + * status code. + */ + snprintf(curl_errorstr, sizeof(curl_errorstr), + "The requested URL returned error: %ld", + results->http_code); + } + + if (results->curl_result == CURLE_OK) { + credential_approve(&http_auth); + return HTTP_OK; + } else if (missing_target(results)) + return HTTP_MISSING_TARGET; + else if (results->http_code == 401) { + if (http_auth.username && http_auth.password) { + credential_reject(&http_auth); + return HTTP_NOAUTH; + } else { +#ifdef LIBCURL_CAN_HANDLE_AUTH_ANY + http_auth_methods &= ~CURLAUTH_GSSNEGOTIATE; +#endif + return HTTP_REAUTH; + } + } else { +#if LIBCURL_VERSION_NUM >= 0x070c00 + if (!curl_errorstr[0]) + strlcpy(curl_errorstr, + curl_easy_strerror(results->curl_result), + sizeof(curl_errorstr)); +#endif + return HTTP_ERROR; + } +} + +int run_one_slot(struct active_request_slot *slot, + struct slot_results *results) +{ + slot->results = results; + if (!start_active_slot(slot)) { + snprintf(curl_errorstr, sizeof(curl_errorstr), + "failed to start HTTP request"); + return HTTP_START_FAILED; + } + + run_active_slot(slot); + return handle_curl_result(results); +} + +static CURLcode curlinfo_strbuf(CURL *curl, CURLINFO info, struct strbuf *buf) +{ + char *ptr; + CURLcode ret; + + strbuf_reset(buf); + ret = curl_easy_getinfo(curl, info, &ptr); + if (!ret && ptr) + strbuf_addstr(buf, ptr); + return ret; +} + +/* + * Check for and extract a content-type parameter. "raw" + * should be positioned at the start of the potential + * parameter, with any whitespace already removed. + * + * "name" is the name of the parameter. The value is appended + * to "out". + */ +static int extract_param(const char *raw, const char *name, + struct strbuf *out) +{ + size_t len = strlen(name); + + if (strncasecmp(raw, name, len)) + return -1; + raw += len; + + if (*raw != '=') + return -1; + raw++; + + while (*raw && !isspace(*raw) && *raw != ';') + strbuf_addch(out, *raw++); + return 0; +} + +/* + * Extract a normalized version of the content type, with any + * spaces suppressed, all letters lowercased, and no trailing ";" + * or parameters. + * + * Note that we will silently remove even invalid whitespace. For + * example, "text / plain" is specifically forbidden by RFC 2616, + * but "text/plain" is the only reasonable output, and this keeps + * our code simple. + * + * If the "charset" argument is not NULL, store the value of any + * charset parameter there. + * + * Example: + * "TEXT/PLAIN; charset=utf-8" -> "text/plain", "utf-8" + * "text / plain" -> "text/plain" + */ +static void extract_content_type(struct strbuf *raw, struct strbuf *type, + struct strbuf *charset) +{ + const char *p; + + strbuf_reset(type); + strbuf_grow(type, raw->len); + for (p = raw->buf; *p; p++) { + if (isspace(*p)) + continue; + if (*p == ';') { + p++; + break; + } + strbuf_addch(type, tolower(*p)); + } + + if (!charset) + return; + + strbuf_reset(charset); + while (*p) { + while (isspace(*p) || *p == ';') + p++; + if (!extract_param(p, "charset", charset)) + return; + while (*p && !isspace(*p)) + p++; + } + + if (!charset->len && starts_with(type->buf, "text/")) + strbuf_addstr(charset, "ISO-8859-1"); +} + +static void write_accept_language(struct strbuf *buf) +{ + /* + * MAX_DECIMAL_PLACES must not be larger than 3. If it is larger than + * that, q-value will be smaller than 0.001, the minimum q-value the + * HTTP specification allows. See + * http://tools.ietf.org/html/rfc7231#section-5.3.1 for q-value. + */ + const int MAX_DECIMAL_PLACES = 3; + const int MAX_LANGUAGE_TAGS = 1000; + const int MAX_ACCEPT_LANGUAGE_HEADER_SIZE = 4000; + char **language_tags = NULL; + int num_langs = 0; + const char *s = get_preferred_languages(); + int i; + struct strbuf tag = STRBUF_INIT; + + /* Don't add Accept-Language header if no language is preferred. */ + if (!s) + return; + + /* + * Split the colon-separated string of preferred languages into + * language_tags array. + */ + do { + /* collect language tag */ + for (; *s && (isalnum(*s) || *s == '_'); s++) + strbuf_addch(&tag, *s == '_' ? '-' : *s); + + /* skip .codeset, @modifier and any other unnecessary parts */ + while (*s && *s != ':') + s++; + + if (tag.len) { + num_langs++; + REALLOC_ARRAY(language_tags, num_langs); + language_tags[num_langs - 1] = strbuf_detach(&tag, NULL); + if (num_langs >= MAX_LANGUAGE_TAGS - 1) /* -1 for '*' */ + break; + } + } while (*s++); + + /* write Accept-Language header into buf */ + if (num_langs) { + int last_buf_len = 0; + int max_q; + int decimal_places; + char q_format[32]; + + /* add '*' */ + REALLOC_ARRAY(language_tags, num_langs + 1); + language_tags[num_langs++] = "*"; /* it's OK; this won't be freed */ + + /* compute decimal_places */ + for (max_q = 1, decimal_places = 0; + max_q < num_langs && decimal_places <= MAX_DECIMAL_PLACES; + decimal_places++, max_q *= 10) + ; + + xsnprintf(q_format, sizeof(q_format), ";q=0.%%0%dd", decimal_places); + + strbuf_addstr(buf, "Accept-Language: "); + + for (i = 0; i < num_langs; i++) { + if (i > 0) + strbuf_addstr(buf, ", "); + + strbuf_addstr(buf, language_tags[i]); + + if (i > 0) + strbuf_addf(buf, q_format, max_q - i); + + if (buf->len > MAX_ACCEPT_LANGUAGE_HEADER_SIZE) { + strbuf_remove(buf, last_buf_len, buf->len - last_buf_len); + break; + } + + last_buf_len = buf->len; + } + } + + /* free language tags -- last one is a static '*' */ + for (i = 0; i < num_langs - 1; i++) + free(language_tags[i]); + free(language_tags); +} + +/* + * Get an Accept-Language header which indicates user's preferred languages. + * + * Examples: + * LANGUAGE= -> "" + * LANGUAGE=ko:en -> "Accept-Language: ko, en; q=0.9, *; q=0.1" + * LANGUAGE=ko_KR.UTF-8:sr@latin -> "Accept-Language: ko-KR, sr; q=0.9, *; q=0.1" + * LANGUAGE=ko LANG=en_US.UTF-8 -> "Accept-Language: ko, *; q=0.1" + * LANGUAGE= LANG=en_US.UTF-8 -> "Accept-Language: en-US, *; q=0.1" + * LANGUAGE= LANG=C -> "" + */ +static const char *get_accept_language(void) +{ + if (!cached_accept_language) { + struct strbuf buf = STRBUF_INIT; + write_accept_language(&buf); + if (buf.len > 0) + cached_accept_language = strbuf_detach(&buf, NULL); + } + + return cached_accept_language; +} + /* http_request() targets */ #define HTTP_REQUEST_STRBUF 0 #define HTTP_REQUEST_FILE 1 -static int http_request(const char *url, void *result, int target, int options) +static int http_request(const char *url, + void *result, int target, + const struct http_get_options *options) { struct active_request_slot *slot; struct slot_results results; struct curl_slist *headers = NULL; struct strbuf buf = STRBUF_INIT; + const char *accept_language; int ret; slot = get_active_slot(); - slot->results = &results; curl_easy_setopt(slot->curl, CURLOPT_HTTPGET, 1); if (result == NULL) { @@ -767,72 +1211,146 @@ static int http_request(const char *url, void *result, int target, int options) fwrite_buffer); } + accept_language = get_accept_language(); + + if (accept_language) + headers = curl_slist_append(headers, accept_language); + strbuf_addstr(&buf, "Pragma:"); - if (options & HTTP_NO_CACHE) + if (options && options->no_cache) strbuf_addstr(&buf, " no-cache"); + if (options && options->keep_error) + curl_easy_setopt(slot->curl, CURLOPT_FAILONERROR, 0); headers = curl_slist_append(headers, buf.buf); curl_easy_setopt(slot->curl, CURLOPT_URL, url); curl_easy_setopt(slot->curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(slot->curl, CURLOPT_ENCODING, "gzip"); - if (start_active_slot(slot)) { - run_active_slot(slot); - if (results.curl_result == CURLE_OK) - ret = HTTP_OK; - else if (missing_target(&results)) - ret = HTTP_MISSING_TARGET; - else if (results.http_code == 401) { - if (http_auth.username && http_auth.password) { - credential_reject(&http_auth); - ret = HTTP_NOAUTH; - } else { - credential_fill(&http_auth); - init_curl_http_auth(slot->curl); - ret = HTTP_REAUTH; - } - } else { - if (!curl_errorstr[0]) - strlcpy(curl_errorstr, - curl_easy_strerror(results.curl_result), - sizeof(curl_errorstr)); - ret = HTTP_ERROR; - } - } else { - error("Unable to start HTTP request for %s", url); - ret = HTTP_START_FAILED; + ret = run_one_slot(slot, &results); + + if (options && options->content_type) { + struct strbuf raw = STRBUF_INIT; + curlinfo_strbuf(slot->curl, CURLINFO_CONTENT_TYPE, &raw); + extract_content_type(&raw, options->content_type, + options->charset); + strbuf_release(&raw); } + if (options && options->effective_url) + curlinfo_strbuf(slot->curl, CURLINFO_EFFECTIVE_URL, + options->effective_url); + curl_slist_free_all(headers); strbuf_release(&buf); - if (ret == HTTP_OK) - credential_approve(&http_auth); - return ret; } -static int http_request_reauth(const char *url, void *result, int target, - int options) +/* + * Update the "base" url to a more appropriate value, as deduced by + * redirects seen when requesting a URL starting with "url". + * + * The "asked" parameter is a URL that we asked curl to access, and must begin + * with "base". + * + * The "got" parameter is the URL that curl reported to us as where we ended + * up. + * + * Returns 1 if we updated the base url, 0 otherwise. + * + * Our basic strategy is to compare "base" and "asked" to find the bits + * specific to our request. We then strip those bits off of "got" to yield the + * new base. So for example, if our base is "http://example.com/foo.git", + * and we ask for "http://example.com/foo.git/info/refs", we might end up + * with "https://other.example.com/foo.git/info/refs". We would want the + * new URL to become "https://other.example.com/foo.git". + * + * Note that this assumes a sane redirect scheme. It's entirely possible + * in the example above to end up at a URL that does not even end in + * "info/refs". In such a case we simply punt, as there is not much we can + * do (and such a scheme is unlikely to represent a real git repository, + * which means we are likely about to abort anyway). + */ +static int update_url_from_redirect(struct strbuf *base, + const char *asked, + const struct strbuf *got) +{ + const char *tail; + size_t tail_len; + + if (!strcmp(asked, got->buf)) + return 0; + + if (!skip_prefix(asked, base->buf, &tail)) + die("BUG: update_url_from_redirect: %s is not a superset of %s", + asked, base->buf); + + tail_len = strlen(tail); + + if (got->len < tail_len || + strcmp(tail, got->buf + got->len - tail_len)) + return 0; /* insane redirect scheme */ + + strbuf_reset(base); + strbuf_add(base, got->buf, got->len - tail_len); + return 1; +} + +static int http_request_reauth(const char *url, + void *result, int target, + struct http_get_options *options) { int ret = http_request(url, result, target, options); + + if (options && options->effective_url && options->base_url) { + if (update_url_from_redirect(options->base_url, + url, options->effective_url)) { + credential_from_url(&http_auth, options->base_url->buf); + url = options->effective_url->buf; + } + } + if (ret != HTTP_REAUTH) return ret; + + /* + * If we are using KEEP_ERROR, the previous request may have + * put cruft into our output stream; we should clear it out before + * making our next request. We only know how to do this for + * the strbuf case, but that is enough to satisfy current callers. + */ + if (options && options->keep_error) { + switch (target) { + case HTTP_REQUEST_STRBUF: + strbuf_reset(result); + break; + default: + die("BUG: HTTP_KEEP_ERROR is only supported with strbufs"); + } + } + + credential_fill(&http_auth); + return http_request(url, result, target, options); } -int http_get_strbuf(const char *url, struct strbuf *result, int options) +int http_get_strbuf(const char *url, + struct strbuf *result, + struct http_get_options *options) { return http_request_reauth(url, result, HTTP_REQUEST_STRBUF, options); } /* - * Downloads an url and stores the result in the given file. + * Downloads a URL and stores the result in the given file. * * If a previous interrupted download is detected (i.e. a previous temporary * file is still around) the download is resumed. */ -static int http_get_file(const char *url, const char *filename, int options) +static int http_get_file(const char *url, const char *filename, + struct http_get_options *options) { int ret; struct strbuf tmpfile = STRBUF_INIT; @@ -840,7 +1358,7 @@ static int http_get_file(const char *url, const char *filename, int options) strbuf_addf(&tmpfile, "%s.temp", filename); result = fopen(tmpfile.buf, "a"); - if (! result) { + if (!result) { error("Unable to open local file %s", tmpfile.buf); ret = HTTP_ERROR; goto cleanup; @@ -849,34 +1367,28 @@ static int http_get_file(const char *url, const char *filename, int options) ret = http_request_reauth(url, result, HTTP_REQUEST_FILE, options); fclose(result); - if ((ret == HTTP_OK) && move_temp_to_file(tmpfile.buf, filename)) + if (ret == HTTP_OK && finalize_object_file(tmpfile.buf, filename)) ret = HTTP_ERROR; cleanup: strbuf_release(&tmpfile); return ret; } -int http_error(const char *url, int ret) -{ - /* http_request has already handled HTTP_START_FAILED. */ - if (ret != HTTP_START_FAILED) - error("%s while accessing %s", curl_errorstr, url); - - return ret; -} - int http_fetch_ref(const char *base, struct ref *ref) { + struct http_get_options options = {0}; char *url; struct strbuf buffer = STRBUF_INIT; int ret = -1; + options.no_cache = 1; + url = quote_ref_url(base, ref->name); - if (http_get_strbuf(url, &buffer, HTTP_NO_CACHE) == HTTP_OK) { + if (http_get_strbuf(url, &buffer, &options) == HTTP_OK) { strbuf_rtrim(&buffer); if (buffer.len == 40) ret = get_sha1_hex(buffer.buf, ref->old_sha1); - else if (!prefixcmp(buffer.buf, "ref: ")) { + else if (starts_with(buffer.buf, "ref: ")) { ref->symref = xstrdup(buffer.buf + 5); ret = 0; } @@ -903,8 +1415,8 @@ static char *fetch_pack_index(unsigned char *sha1, const char *base_url) strbuf_addf(&buf, "%s.temp", sha1_pack_index_name(sha1)); tmp = strbuf_detach(&buf, NULL); - if (http_get_file(url, tmp, 0) != HTTP_OK) { - error("Unable to get pack index %s\n", url); + if (http_get_file(url, tmp, NULL) != HTTP_OK) { + error("Unable to get pack index %s", url); free(tmp); tmp = NULL; } @@ -921,7 +1433,7 @@ static int fetch_and_setup_pack_index(struct packed_git **packs_head, int ret; if (has_pack_index(sha1)) { - new_pack = parse_pack_index(sha1, NULL); + new_pack = parse_pack_index(sha1, sha1_pack_index_name(sha1)); if (!new_pack) return -1; /* parse_pack_index() already issued error message */ goto add_pack; @@ -942,7 +1454,7 @@ static int fetch_and_setup_pack_index(struct packed_git **packs_head, ret = verify_pack_index(new_pack); if (!ret) { close_pack_index(new_pack); - ret = move_temp_to_file(tmp_idx, sha1_pack_index_name(sha1)); + ret = finalize_object_file(tmp_idx, sha1_pack_index_name(sha1)); } free(tmp_idx); if (ret) @@ -956,6 +1468,7 @@ add_pack: int http_get_info_packs(const char *base_url, struct packed_git **packs_head) { + struct http_get_options options = {0}; int ret = 0, i = 0; char *url, *data; struct strbuf buf = STRBUF_INIT; @@ -965,7 +1478,8 @@ int http_get_info_packs(const char *base_url, struct packed_git **packs_head) strbuf_addstr(&buf, "objects/info/packs"); url = strbuf_detach(&buf, NULL); - ret = http_get_strbuf(url, &buf, HTTP_NO_CACHE); + options.no_cache = 1; + ret = http_get_strbuf(url, &buf, &options); if (ret != HTTP_OK) goto cleanup; @@ -975,8 +1489,8 @@ int http_get_info_packs(const char *base_url, struct packed_git **packs_head) case 'P': i++; if (i + 52 <= buf.len && - !prefixcmp(data + i, " pack-") && - !prefixcmp(data + i + 46, ".pack\n")) { + starts_with(data + i, " pack-") && + starts_with(data + i + 46, ".pack\n")) { get_sha1_hex(data + i + 6, sha1); fetch_and_setup_pack_index(packs_head, sha1, base_url); @@ -1007,6 +1521,7 @@ void release_http_pack_request(struct http_pack_request *preq) } preq->slot = NULL; free(preq->url); + free(preq); } int finish_http_pack_request(struct http_pack_request *preq) @@ -1014,7 +1529,8 @@ int finish_http_pack_request(struct http_pack_request *preq) struct packed_git **lst; struct packed_git *p = preq->target; char *tmp_idx; - struct child_process ip; + size_t len; + struct child_process ip = CHILD_PROCESS_INIT; const char *ip_argv[8]; close_pack_index(p); @@ -1027,9 +1543,9 @@ int finish_http_pack_request(struct http_pack_request *preq) lst = &((*lst)->next); *lst = (*lst)->next; - tmp_idx = xstrdup(preq->tmpfile); - strcpy(tmp_idx + strlen(tmp_idx) - strlen(".pack.temp"), - ".idx.temp"); + if (!strip_suffix(preq->tmpfile, ".pack.temp", &len)) + die("BUG: pack tmpfile does not end in .pack.temp?"); + tmp_idx = xstrfmt("%.*s.idx.temp", (int)len, preq->tmpfile); ip_argv[0] = "index-pack"; ip_argv[1] = "-o"; @@ -1037,7 +1553,6 @@ int finish_http_pack_request(struct http_pack_request *preq) ip_argv[3] = preq->tmpfile; ip_argv[4] = NULL; - memset(&ip, 0, sizeof(ip)); ip.argv = ip_argv; ip.git_cmd = 1; ip.no_stdin = 1; @@ -1052,8 +1567,8 @@ int finish_http_pack_request(struct http_pack_request *preq) unlink(sha1_pack_index_name(p->sha1)); - if (move_temp_to_file(preq->tmpfile, sha1_pack_name(p->sha1)) - || move_temp_to_file(tmp_idx, sha1_pack_index_name(p->sha1))) { + if (finalize_object_file(preq->tmpfile, sha1_pack_name(p->sha1)) + || finalize_object_file(tmp_idx, sha1_pack_index_name(p->sha1))) { free(tmp_idx); return -1; } @@ -1105,7 +1620,7 @@ struct http_pack_request *new_http_pack_request( fprintf(stderr, "Resuming fetch of pack %s at byte %ld\n", sha1_to_hex(target->sha1), prev_posn); - sprintf(range, "Range: bytes=%ld-", prev_posn); + xsnprintf(range, sizeof(range), "Range: bytes=%ld-", prev_posn); preq->range_header = curl_slist_append(NULL, range); curl_easy_setopt(preq->slot->curl, CURLOPT_HTTPHEADER, preq->range_header); @@ -1152,7 +1667,7 @@ struct http_object_request *new_http_object_request(const char *base_url, unsigned char *sha1) { char *hex = sha1_to_hex(sha1); - char *filename; + const char *filename; char prevfile[PATH_MAX]; int prevlocal; char prev_buf[PREV_BUF_SIZE]; @@ -1265,7 +1780,7 @@ struct http_object_request *new_http_object_request(const char *base_url, fprintf(stderr, "Resuming fetch of object %s at byte %ld\n", hex, prev_posn); - sprintf(range, "Range: bytes=%ld-", prev_posn); + xsnprintf(range, sizeof(range), "Range: bytes=%ld-", prev_posn); range_header = curl_slist_append(range_header, range); curl_easy_setopt(freq->slot->curl, CURLOPT_HTTPHEADER, range_header); @@ -1317,7 +1832,7 @@ int finish_http_object_request(struct http_object_request *freq) return -1; } freq->rename = - move_temp_to_file(freq->tmpfile, sha1_file_name(freq->sha1)); + finalize_object_file(freq->tmpfile, sha1_file_name(freq->sha1)); return freq->rename; } |