/* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include "md_http.h" #include "md_log.h" #include "md_util.h" #include "md_curl.h" /**************************************************************************************************/ /* md_http curl implementation */ static apr_status_t curl_status(unsigned int curl_code) { switch (curl_code) { case CURLE_OK: return APR_SUCCESS; case CURLE_UNSUPPORTED_PROTOCOL: return APR_ENOTIMPL; case CURLE_NOT_BUILT_IN: return APR_ENOTIMPL; case CURLE_URL_MALFORMAT: return APR_EINVAL; case CURLE_COULDNT_RESOLVE_PROXY:return APR_ECONNREFUSED; case CURLE_COULDNT_RESOLVE_HOST: return APR_ECONNREFUSED; case CURLE_COULDNT_CONNECT: return APR_ECONNREFUSED; case CURLE_REMOTE_ACCESS_DENIED: return APR_EACCES; case CURLE_OUT_OF_MEMORY: return APR_ENOMEM; case CURLE_OPERATION_TIMEDOUT: return APR_TIMEUP; case CURLE_SSL_CONNECT_ERROR: return APR_ECONNABORTED; case CURLE_AGAIN: return APR_EAGAIN; default: return APR_EGENERAL; } } typedef struct { CURL *curl; CURLM *curlm; struct curl_slist *req_hdrs; md_http_response_t *response; apr_status_t rv; int status_fired; } md_curl_internals_t; static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton) { apr_bucket_brigade *body = baton; size_t blen, read_len = 0, max_len = len * nmemb; const char *bdata; char *rdata = data; apr_bucket *b; apr_status_t rv; while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) { b = APR_BRIGADE_FIRST(body); if (APR_BUCKET_IS_METADATA(b)) { if (APR_BUCKET_IS_EOS(b)) { body = NULL; } } else { rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ); if (rv == APR_SUCCESS) { if (blen > max_len) { apr_bucket_split(b, max_len); blen = max_len; } memcpy(rdata, bdata, blen); read_len += blen; max_len -= blen; rdata += blen; } else { body = NULL; if (!APR_STATUS_IS_EOF(rv)) { /* everything beside EOF is an error */ read_len = CURL_READFUNC_ABORT; } } } apr_bucket_delete(b); } return read_len; } static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton) { md_curl_internals_t *internals = baton; md_http_response_t *res = internals->response; size_t blen = len * nmemb; apr_status_t rv; if (res->body) { if (res->req->resp_limit) { apr_off_t body_len = 0; apr_brigade_length(res->body, 0, &body_len); if (body_len + (apr_off_t)blen > res->req->resp_limit) { return 0; /* signal curl failure */ } } rv = apr_brigade_write(res->body, NULL, NULL, (const char *)data, blen); if (rv != APR_SUCCESS) { /* returning anything != blen will make CURL fail this */ return 0; } } return blen; } static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton) { md_curl_internals_t *internals = baton; md_http_response_t *res = internals->response; size_t len, clen = elen * nmemb; const char *name = NULL, *value = "", *b = buffer; apr_size_t i; len = (clen && b[clen-1] == '\n')? clen-1 : clen; len = (len && b[len-1] == '\r')? len-1 : len; for (i = 0; i < len; ++i) { if (b[i] == ':') { name = apr_pstrndup(res->req->pool, b, i); ++i; while (i < len && b[i] == ' ') { ++i; } if (i < len) { value = apr_pstrndup(res->req->pool, b+i, len - i); } break; } } if (name != NULL) { apr_table_add(res->headers, name, value); } return clen; } typedef struct { md_http_request_t *req; struct curl_slist *hdrs; apr_status_t rv; } curlify_hdrs_ctx; static int curlify_headers(void *baton, const char *key, const char *value) { curlify_hdrs_ctx *ctx = baton; const char *s; if (strchr(key, '\r') || strchr(key, '\n') || strchr(value, '\r') || strchr(value, '\n')) { ctx->rv = APR_EINVAL; return 0; } s = apr_psprintf(ctx->req->pool, "%s: %s", key, value); ctx->hdrs = curl_slist_append(ctx->hdrs, s); return 1; } /* Convert timeout values for curl. Since curl uses 0 to disable * timeout, return at least 1 if the apr_time_t value is non-zero. */ static long timeout_msec(apr_time_t timeout) { long ms = (long)apr_time_as_msec(timeout); return ms? ms : (timeout? 1 : 0); } static long timeout_sec(apr_time_t timeout) { long s = (long)apr_time_sec(timeout); return s? s : (timeout? 1 : 0); } static int curl_debug_log(CURL *curl, curl_infotype type, char *data, size_t size, void *baton) { md_http_request_t *req = baton; (void)curl; switch (type) { case CURLINFO_TEXT: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, "req[%d]: info %s", req->id, apr_pstrndup(req->pool, data, size)); break; case CURLINFO_HEADER_OUT: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, "req[%d]: header --> %s", req->id, apr_pstrndup(req->pool, data, size)); break; case CURLINFO_HEADER_IN: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, "req[%d]: header <-- %s", req->id, apr_pstrndup(req->pool, data, size)); break; case CURLINFO_DATA_OUT: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, "req[%d]: data --> %ld bytes", req->id, (long)size); if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { md_data_t d; const char *s; md_data_init(&d, data, size); md_data_to_hex(&s, 0, req->pool, &d); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, "req[%d]: data(hex) --> %s", req->id, s); } break; case CURLINFO_DATA_IN: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->pool, "req[%d]: data <-- %ld bytes", req->id, (long)size); if (md_log_is_level(req->pool, MD_LOG_TRACE5)) { md_data_t d; const char *s; md_data_init(&d, data, size); md_data_to_hex(&s, 0, req->pool, &d); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE5, 0, req->pool, "req[%d]: data(hex) <-- %s", req->id, s); } break; default: break; } return 0; } static apr_status_t internals_setup(md_http_request_t *req) { md_curl_internals_t *internals; CURL *curl; apr_status_t rv = APR_SUCCESS; curl = md_http_get_impl_data(req->http); if (!curl) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "creating curl instance"); curl = curl_easy_init(); if (!curl) { rv = APR_EGENERAL; goto leave; } curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb); curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL); curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb); curl_easy_setopt(curl, CURLOPT_READDATA, NULL); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL); } else { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "reusing curl instance from http"); } internals = apr_pcalloc(req->pool, sizeof(*internals)); internals->curl = curl; internals->response = apr_pcalloc(req->pool, sizeof(md_http_response_t)); internals->response->req = req; internals->response->status = 400; internals->response->headers = apr_table_make(req->pool, 5); internals->response->body = apr_brigade_create(req->pool, req->bucket_alloc); curl_easy_setopt(curl, CURLOPT_URL, req->url); if (!apr_strnatcasecmp("GET", req->method)) { /* nop */ } else if (!apr_strnatcasecmp("HEAD", req->method)) { curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); } else if (!apr_strnatcasecmp("POST", req->method)) { curl_easy_setopt(curl, CURLOPT_POST, 1L); } else { curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); } curl_easy_setopt(curl, CURLOPT_HEADERDATA, internals); curl_easy_setopt(curl, CURLOPT_READDATA, req->body); curl_easy_setopt(curl, CURLOPT_WRITEDATA, internals); if (req->timeout.overall > 0) { curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_msec(req->timeout.overall)); } if (req->timeout.connect > 0) { curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_msec(req->timeout.connect)); } if (req->timeout.stalled > 0) { curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, req->timeout.stall_bytes_per_sec); curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, timeout_sec(req->timeout.stalled)); } if (req->ca_file) { curl_easy_setopt(curl, CURLOPT_CAINFO, req->ca_file); } if (req->unix_socket_path) { curl_easy_setopt(curl, CURLOPT_UNIX_SOCKET_PATH, req->unix_socket_path); } if (req->body_len >= 0) { /* set the Content-Length */ curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)req->body_len); } if (req->user_agent) { curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent); } if (req->proxy_url) { curl_easy_setopt(curl, CURLOPT_PROXY, req->proxy_url); } if (!apr_is_empty_table(req->headers)) { curlify_hdrs_ctx ctx; ctx.req = req; ctx.hdrs = NULL; ctx.rv = APR_SUCCESS; apr_table_do(curlify_headers, &ctx, req->headers, NULL); internals->req_hdrs = ctx.hdrs; if (ctx.rv == APR_SUCCESS) { curl_easy_setopt(curl, CURLOPT_HTTPHEADER, internals->req_hdrs); } } md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "req[%d]: %s %s", req->id, req->method, req->url); if (md_log_is_level(req->pool, MD_LOG_TRACE4)) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_log); curl_easy_setopt(curl, CURLOPT_DEBUGDATA, req); } leave: req->internals = (APR_SUCCESS == rv)? internals : NULL; return rv; } static apr_status_t update_status(md_http_request_t *req) { md_curl_internals_t *internals = req->internals; long l; apr_status_t rv = APR_SUCCESS; if (internals) { rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); if (APR_SUCCESS == rv) { internals->response->status = (int)l; md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool, "req[%d]: http status is %d", req->id, internals->response->status); } } return rv; } static void fire_status(md_http_request_t *req, apr_status_t rv) { md_curl_internals_t *internals = req->internals; if (internals && !internals->status_fired) { internals->status_fired = 1; md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, req->pool, "req[%d] fire callbacks", req->id); if ((APR_SUCCESS == rv) && req->cb.on_response) { rv = req->cb.on_response(internals->response, req->cb.on_response_data); } internals->rv = rv; if (req->cb.on_status) { req->cb.on_status(req, rv, req->cb.on_status_data); } } } static apr_status_t md_curl_perform(md_http_request_t *req) { apr_status_t rv = APR_SUCCESS; CURLcode curle; md_curl_internals_t *internals; long l; if (APR_SUCCESS != (rv = internals_setup(req))) goto leave; internals = req->internals; curle = curl_easy_perform(internals->curl); rv = curl_status(curle); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->pool, "request failed(%d): %s", curle, curl_easy_strerror(curle)); goto leave; } rv = curl_status(curl_easy_getinfo(internals->curl, CURLINFO_RESPONSE_CODE, &l)); if (APR_SUCCESS == rv) { internals->response->status = (int)l; } md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->pool, "request <-- %d", internals->response->status); if (req->cb.on_response) { rv = req->cb.on_response(internals->response, req->cb.on_response_data); req->cb.on_response = NULL; } leave: fire_status(req, rv); md_http_req_destroy(req); return rv; } static md_http_request_t *find_curl_request(apr_array_header_t *requests, CURL *curl) { md_http_request_t *req; md_curl_internals_t *internals; int i; for (i = 0; i < requests->nelts; ++i) { req = APR_ARRAY_IDX(requests, i, md_http_request_t*); internals = req->internals; if (internals && internals->curl == curl) { return req; } } return NULL; } static void add_to_curlm(md_http_request_t *req, CURLM *curlm) { md_curl_internals_t *internals = req->internals; assert(curlm); assert(internals); if (internals->curlm == NULL) { internals->curlm = curlm; } assert(internals->curlm == curlm); curl_multi_add_handle(curlm, internals->curl); } static void remove_from_curlm_and_destroy(md_http_request_t *req, CURLM *curlm) { md_curl_internals_t *internals = req->internals; assert(curlm); assert(internals); assert(internals->curlm == curlm); curl_multi_remove_handle(curlm, internals->curl); internals->curlm = NULL; md_http_req_destroy(req); } static apr_status_t md_curl_multi_perform(md_http_t *http, apr_pool_t *p, md_http_next_req *nextreq, void *baton) { md_http_t *sub_http; md_http_request_t *req; CURLM *curlm = NULL; CURLMcode mc; struct CURLMsg *curlmsg; apr_array_header_t *http_spares; apr_array_header_t *requests; int i, running, numfds, slowdown, msgcount; apr_status_t rv; http_spares = apr_array_make(p, 10, sizeof(md_http_t*)); requests = apr_array_make(p, 10, sizeof(md_http_request_t*)); curlm = curl_multi_init(); if (!curlm) { rv = APR_ENOMEM; goto leave; } running = 1; slowdown = 0; while(1) { while (1) { /* fetch as many requests as nextreq gives us */ if (http_spares->nelts > 0) { sub_http = *(md_http_t **)(apr_array_pop(http_spares)); } else { rv = md_http_clone(&sub_http, p, http); if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "multi_perform[%d reqs]: setup failed", requests->nelts); goto leave; } } rv = nextreq(&req, baton, sub_http, requests->nelts); if (APR_STATUS_IS_ENOENT(rv)) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "multi_perform[%d reqs]: no more requests", requests->nelts); if (!requests->nelts) { goto leave; } break; } else if (APR_SUCCESS != rv) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "multi_perform[%d reqs]: nextreq() failed", requests->nelts); APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; goto leave; } if (APR_SUCCESS != (rv = internals_setup(req))) { if (req->cb.on_status) req->cb.on_status(req, rv, req->cb.on_status_data); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "multi_perform[%d reqs]: setup failed", requests->nelts); APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; goto leave; } APR_ARRAY_PUSH(requests, md_http_request_t*) = req; add_to_curlm(req, curlm); md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "multi_perform[%d reqs]: added request", requests->nelts); } mc = curl_multi_perform(curlm, &running); if (CURLM_OK == mc) { mc = curl_multi_wait(curlm, NULL, 0, 1000, &numfds); if (numfds) slowdown = 0; } if (CURLM_OK != mc) { rv = APR_ECONNABORTED; md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "multi_perform[%d reqs] failed(%d): %s", requests->nelts, mc, curl_multi_strerror(mc)); goto leave; } if (!numfds) { /* no activity on any connection, timeout */ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "multi_perform[%d reqs]: slowdown %d", requests->nelts, slowdown); if (slowdown) apr_sleep(apr_time_from_msec(100)); ++slowdown; } /* process status messages, e.g. that a request is done */ while (running < requests->nelts) { curlmsg = curl_multi_info_read(curlm, &msgcount); if (!curlmsg) break; if (curlmsg->msg == CURLMSG_DONE) { req = find_curl_request(requests, curlmsg->easy_handle); if (req) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, p, "multi_perform[%d reqs]: req[%d] done", requests->nelts, req->id); update_status(req); fire_status(req, curl_status(curlmsg->data.result)); md_array_remove(requests, req); sub_http = req->http; APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; remove_from_curlm_and_destroy(req, curlm); } else { md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "multi_perform[%d reqs]: req done, but not found by handle", requests->nelts); } } } }; leave: md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "multi_perform[%d reqs]: leaving", requests->nelts); for (i = 0; i < requests->nelts; ++i) { req = APR_ARRAY_IDX(requests, i, md_http_request_t*); fire_status(req, APR_SUCCESS); sub_http = req->http; APR_ARRAY_PUSH(http_spares, md_http_t*) = sub_http; remove_from_curlm_and_destroy(req, curlm); } if (curlm) curl_multi_cleanup(curlm); return rv; } static int initialized; static apr_status_t md_curl_init(void) { if (!initialized) { initialized = 1; curl_global_init(CURL_GLOBAL_DEFAULT); } return APR_SUCCESS; } static void md_curl_req_cleanup(md_http_request_t *req) { md_curl_internals_t *internals = req->internals; if (internals) { if (internals->curl) { CURL *curl = md_http_get_impl_data(req->http); if (curl == internals->curl) { /* NOP: we have this curl at the md_http_t already */ } else if (!curl) { /* no curl at the md_http_t yet, install this one */ md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->pool, "register curl instance at http"); md_http_set_impl_data(req->http, internals->curl); } else { /* There already is a curl at the md_http_t and it's not this one. */ curl_easy_cleanup(internals->curl); } } if (internals->req_hdrs) curl_slist_free_all(internals->req_hdrs); req->internals = NULL; } } static void md_curl_cleanup(md_http_t *http, apr_pool_t *pool) { CURL *curl; curl = md_http_get_impl_data(http); if (curl) { md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, pool, "cleanup curl instance"); md_http_set_impl_data(http, NULL); curl_easy_cleanup(curl); } } static md_http_impl_t impl = { md_curl_init, md_curl_req_cleanup, md_curl_perform, md_curl_multi_perform, md_curl_cleanup, }; md_http_impl_t * md_curl_get_impl(apr_pool_t *p) { /* trigger early global curl init, before we are down a rabbit hole */ (void)p; md_curl_init(); return &impl; }