diff options
-rw-r--r-- | doc/docstrings/curl_duphandle.rst | 23 | ||||
-rw-r--r-- | src/easy.c | 422 | ||||
-rw-r--r-- | src/easyopt.c | 81 | ||||
-rw-r--r-- | src/module.c | 13 | ||||
-rw-r--r-- | src/pycurl.h | 53 | ||||
-rw-r--r-- | tests/app.py | 3 | ||||
-rw-r--r-- | tests/duphandle_test.py | 144 |
7 files changed, 635 insertions, 104 deletions
diff --git a/doc/docstrings/curl_duphandle.rst b/doc/docstrings/curl_duphandle.rst new file mode 100644 index 0000000..ca6e7a8 --- /dev/null +++ b/doc/docstrings/curl_duphandle.rst @@ -0,0 +1,23 @@ +duphandle() -> Curl + +Clone a curl handle. This function will return a new curl handle, +a duplicate, using all the options previously set in the input curl handle. +Both handles can subsequently be used independently. + +The new handle will not inherit any state information, no connections, +no SSL sessions and no cookies. It also will not inherit any share object +states or options (it will be made as if SHARE was unset). + +Corresponds to `curl_easy_duphandle`_ in libcurl. + +Example usage:: + + import pycurl + curl = pycurl.Curl() + curl.setopt(pycurl.URL, "https://python.org") + dup = curl.duphandle() + curl.perform() + dup.perform() + +.. _curl_easy_duphandle: + https://curl.se/libcurl/c/curl_easy_duphandle.html @@ -1,6 +1,179 @@ #include "pycurl.h" #include "docstrings.h" + +/************************************************************************* +// CurlSlistObject +**************************************************************************/ + +PYCURL_INTERNAL void +util_curlslist_update(CurlSlistObject **old, struct curl_slist *slist) +{ + /* Decref previous object */ + Py_XDECREF(*old); + /* Create a new object */ + *old = PyObject_New(CurlSlistObject, p_CurlSlist_Type); + assert(*old != NULL); + /* Store curl_slist into the new object */ + (*old)->slist = slist; +} + +PYCURL_INTERNAL void +do_curlslist_dealloc(CurlSlistObject *self) { + if (self->slist != NULL) { + curl_slist_free_all(self->slist); + self->slist = NULL; + } + CurlSlist_Type.tp_free(self); +} + +PYCURL_INTERNAL PyTypeObject CurlSlist_Type = { +#if PY_MAJOR_VERSION >= 3 + PyVarObject_HEAD_INIT(NULL, 0) +#else + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ +#endif + "pycurl.CurlSlist", /* tp_name */ + sizeof(CurlSlistObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)do_curlslist_dealloc, /* tp_dealloc */ + 0, /* tp_print / tp_vectorcall_offset */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved / tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + 0, /* tp_flags */ + 0, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ + 0, /* tp_free */ + 0, /* tp_is_gc */ + 0, /* tp_bases */ + 0, /* tp_mro */ + 0, /* tp_cache */ + 0, /* tp_subclasses */ + 0, /* tp_weaklist */ +#if PY_MAJOR_VERSION >= 3 + 0, /* tp_del */ + 0, /* tp_version_tag */ + 0, /* tp_finalize */ + 0, /* tp_vectorcall */ +#endif +}; + + +/************************************************************************* +// CurlHttppostObject +**************************************************************************/ + +PYCURL_INTERNAL void +util_curlhttppost_update(CurlObject *obj, struct curl_httppost *httppost, PyObject *reflist) +{ + /* Decref previous object */ + Py_XDECREF(obj->httppost); + /* Create a new object */ + obj->httppost = PyObject_New(CurlHttppostObject, p_CurlHttppost_Type); + assert(obj->httppost != NULL); + /* Store curl_httppost and reflist into the new object */ + obj->httppost->httppost = httppost; + obj->httppost->reflist = reflist; +} + +PYCURL_INTERNAL void +do_curlhttppost_dealloc(CurlHttppostObject *self) { + if (self->httppost != NULL) { + curl_formfree(self->httppost); + self->httppost = NULL; + } + Py_CLEAR(self->reflist); + CurlHttppost_Type.tp_free(self); +} + +PYCURL_INTERNAL PyTypeObject CurlHttppost_Type = { +#if PY_MAJOR_VERSION >= 3 + PyVarObject_HEAD_INIT(NULL, 0) +#else + PyObject_HEAD_INIT(NULL) + 0, /* ob_size */ +#endif + "pycurl.CurlHttppost", /* tp_name */ + sizeof(CurlHttppostObject), /* tp_basicsize */ + 0, /* tp_itemsize */ + (destructor)do_curlhttppost_dealloc, /* tp_dealloc */ + 0, /* tp_print / tp_vectorcall_offset */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved / tp_as_async */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + 0, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + 0, /* tp_flags */ + 0, /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ + 0, /* tp_free */ + 0, /* tp_is_gc */ + 0, /* tp_bases */ + 0, /* tp_mro */ + 0, /* tp_cache */ + 0, /* tp_subclasses */ + 0, /* tp_weaklist */ +#if PY_MAJOR_VERSION >= 3 + 0, /* tp_del */ + 0, /* tp_version_tag */ + 0, /* tp_finalize */ + 0, /* tp_vectorcall */ +#endif +}; + + /************************************************************************* // static utility functions **************************************************************************/ @@ -123,6 +296,156 @@ error: return NULL; } +/* duphandle */ +PYCURL_INTERNAL CurlObject * +do_curl_duphandle(CurlObject *self) +{ + PyTypeObject *subtype; + CurlObject *dup; + int res; + int *ptr; + + /* Allocate python curl object */ + subtype = Py_TYPE(self); + dup = (CurlObject *) subtype->tp_alloc(subtype, 0); + if (dup == NULL) + return NULL; + + /* tp_alloc is expected to return zeroed memory */ + for (ptr = (int *) &dup->dict; + ptr < (int *) (((char *) dup) + sizeof(CurlObject)); + ++ptr) + assert(*ptr == 0); + + /* Clone the curl handle */ + dup->handle = curl_easy_duphandle(self->handle); + if (dup->handle == NULL) + goto error; + + /* Set curl error buffer and zero it */ + res = curl_easy_setopt(dup->handle, CURLOPT_ERRORBUFFER, dup->error); + if (res != CURLE_OK) { + goto error; + } + memset(dup->error, 0, sizeof(dup->error)); + + /* Set backreference */ + res = curl_easy_setopt(dup->handle, CURLOPT_PRIVATE, (char *) dup); + if (res != CURLE_OK) { + goto error; + } + + /* Copy attribute dictionary */ + if (self->dict != NULL) { + dup->dict = PyDict_Copy(self->dict); + if (dup->dict == NULL) { + goto error; + } + } + + /* Checking for CURLE_OK is not required here. + * All values have already been successfuly setopt'ed with self->handle. */ + + /* Assign and incref python callback and update data pointers */ + if (self->w_cb != NULL) { + dup->w_cb = my_Py_NewRef(self->w_cb); + curl_easy_setopt(dup->handle, CURLOPT_WRITEDATA, dup); + } + if (self->h_cb != NULL) { + dup->h_cb = my_Py_NewRef(self->h_cb); + curl_easy_setopt(dup->handle, CURLOPT_WRITEHEADER, dup); + } + if (self->r_cb != NULL) { + dup->r_cb = my_Py_NewRef(self->r_cb); + curl_easy_setopt(dup->handle, CURLOPT_READDATA, dup); + } + if (self->pro_cb != NULL) { + dup->pro_cb = my_Py_NewRef(self->pro_cb); + curl_easy_setopt(dup->handle, CURLOPT_PROGRESSDATA, dup); + } +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 32, 0) + if (self->xferinfo_cb != NULL) { + dup->xferinfo_cb = my_Py_NewRef(self->xferinfo_cb); + curl_easy_setopt(dup->handle, CURLOPT_XFERINFODATA, dup); + } +#endif + if (self->debug_cb != NULL) { + dup->debug_cb = my_Py_NewRef(self->debug_cb); + curl_easy_setopt(dup->handle, CURLOPT_DEBUGDATA, dup); + } + if (self->ioctl_cb != NULL) { + dup->ioctl_cb = my_Py_NewRef(self->ioctl_cb); + curl_easy_setopt(dup->handle, CURLOPT_IOCTLDATA, dup); + } + if (self->opensocket_cb != NULL) { + dup->opensocket_cb = my_Py_NewRef(self->opensocket_cb); + curl_easy_setopt(dup->handle, CURLOPT_OPENSOCKETDATA, dup); + } +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 21, 7) + if (self->closesocket_cb != NULL) { + dup->closesocket_cb = my_Py_NewRef(self->closesocket_cb); + curl_easy_setopt(dup->handle, CURLOPT_CLOSESOCKETDATA, dup); + } +#endif + if (self->sockopt_cb != NULL) { + dup->sockopt_cb = my_Py_NewRef(self->sockopt_cb); + curl_easy_setopt(dup->handle, CURLOPT_SOCKOPTDATA, dup); + } +#ifdef HAVE_CURL_7_19_6_OPTS + if (self->ssh_key_cb != NULL) { + dup->ssh_key_cb = my_Py_NewRef(self->ssh_key_cb); + curl_easy_setopt(dup->handle, CURLOPT_SSH_KEYDATA, dup); + } +#endif + if (self->seek_cb != NULL) { + dup->seek_cb = my_Py_NewRef(self->seek_cb); + curl_easy_setopt(dup->handle, CURLOPT_SEEKDATA, dup); + } + + /* Assign and incref python file objects */ + dup->readdata_fp = my_Py_XNewRef(self->readdata_fp); + dup->writedata_fp = my_Py_XNewRef(self->writedata_fp); + dup->writeheader_fp = my_Py_XNewRef(self->writeheader_fp); + + /* Assign and incref postfields object */ + dup->postfields_obj = my_Py_XNewRef(self->postfields_obj); + + /* Assign and incref ca certs related references */ + dup->ca_certs_obj = my_Py_XNewRef(self->ca_certs_obj); + + /* Assign and incref every curl_slist allocated by setopt */ + dup->httpheader = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->httpheader); +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) + dup->proxyheader = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->proxyheader); +#endif + dup->http200aliases = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->http200aliases); + dup->quote = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->quote); + dup->postquote = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->postquote); + dup->prequote = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->prequote); + dup->telnetoptions = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->telnetoptions); +#ifdef HAVE_CURLOPT_RESOLVE + dup->resolve = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->resolve); +#endif +#ifdef HAVE_CURL_7_20_0_OPTS + dup->mail_rcpt = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->mail_rcpt); +#endif +#ifdef HAVE_CURLOPT_CONNECT_TO + dup->connect_to = (CurlSlistObject *)my_Py_XNewRef((PyObject *)self->connect_to); +#endif + + /* Assign and incref httppost */ + dup->httppost = (CurlHttppostObject *)my_Py_XNewRef((PyObject *)self->httppost); + + /* Success - return cloned object */ + return dup; + +error: + Py_CLEAR(dup->dict); + Py_DECREF(dup); /* this also closes dup->handle */ + PyErr_SetString(ErrorObject, "cloning curl failed"); + return NULL; +} + /* util function shared by close() and clear() */ PYCURL_INTERNAL void @@ -197,14 +520,36 @@ util_curl_xdecref(CurlObject *self, int flags, CURL *handle) } if (flags & PYCURL_MEMGROUP_HTTPPOST) { - /* Decrement refcounts for httppost related references. */ - Py_CLEAR(self->httppost_ref_list); + /* Decrement refcounts for httppost object. */ + Py_CLEAR(self->httppost); } if (flags & PYCURL_MEMGROUP_CACERTS) { /* Decrement refcounts for ca certs related references. */ Py_CLEAR(self->ca_certs_obj); } + + if (flags & PYCURL_MEMGROUP_SLIST) { + /* Decrement refcounts for slist objects. */ + Py_CLEAR(self->httpheader); +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) + Py_CLEAR(self->proxyheader); +#endif + Py_CLEAR(self->http200aliases); + Py_CLEAR(self->quote); + Py_CLEAR(self->postquote); + Py_CLEAR(self->prequote); + Py_CLEAR(self->telnetoptions); +#ifdef HAVE_CURLOPT_RESOLVE + Py_CLEAR(self->resolve); +#endif +#ifdef HAVE_CURL_7_20_0_OPTS + Py_CLEAR(self->mail_rcpt); +#endif +#ifdef HAVE_CURLOPT_CONNECT_TO + Py_CLEAR(self->connect_to); +#endif + } } @@ -250,32 +595,6 @@ util_curl_close(CurlObject *self) if (self->weakreflist != NULL) { PyObject_ClearWeakRefs((PyObject *) self); } - - /* Free all variables allocated by setopt */ -#undef SFREE -#define SFREE(v) if ((v) != NULL) (curl_formfree(v), (v) = NULL) - SFREE(self->httppost); -#undef SFREE -#define SFREE(v) if ((v) != NULL) (curl_slist_free_all(v), (v) = NULL) - SFREE(self->httpheader); -#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) - SFREE(self->proxyheader); -#endif - SFREE(self->http200aliases); - SFREE(self->quote); - SFREE(self->postquote); - SFREE(self->prequote); - SFREE(self->telnetoptions); -#ifdef HAVE_CURLOPT_RESOLVE - SFREE(self->resolve); -#endif -#ifdef HAVE_CURL_7_20_0_OPTS - SFREE(self->mail_rcpt); -#endif -#ifdef HAVE_CURLOPT_CONNECT_TO - SFREE(self->connect_to); -#endif -#undef SFREE } @@ -354,6 +673,27 @@ do_curl_traverse(CurlObject *self, visitproc visit, void *arg) VISIT(self->ca_certs_obj); + VISIT((PyObject *) self->httpheader); +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) + VISIT((PyObject *) self->proxyheader); +#endif + VISIT((PyObject *) self->http200aliases); + VISIT((PyObject *) self->quote); + VISIT((PyObject *) self->postquote); + VISIT((PyObject *) self->prequote); + VISIT((PyObject *) self->telnetoptions); +#ifdef HAVE_CURLOPT_RESOLVE + VISIT((PyObject *) self->resolve); +#endif +#ifdef HAVE_CURL_7_20_0_OPTS + VISIT((PyObject *) self->mail_rcpt); +#endif +#ifdef HAVE_CURLOPT_CONNECT_TO + VISIT((PyObject *) self->connect_to); +#endif + + VISIT((PyObject *) self->httppost); + return 0; #undef VISIT } @@ -371,31 +711,6 @@ do_curl_reset(CurlObject *self) /* Decref easy interface related objects */ util_curl_xdecref(self, PYCURL_MEMGROUP_EASY, self->handle); - /* Free all variables allocated by setopt */ -#undef SFREE -#define SFREE(v) if ((v) != NULL) (curl_formfree(v), (v) = NULL) - SFREE(self->httppost); -#undef SFREE -#define SFREE(v) if ((v) != NULL) (curl_slist_free_all(v), (v) = NULL) - SFREE(self->httpheader); -#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) - SFREE(self->proxyheader); -#endif - SFREE(self->http200aliases); - SFREE(self->quote); - SFREE(self->postquote); - SFREE(self->prequote); - SFREE(self->telnetoptions); -#ifdef HAVE_CURLOPT_RESOLVE - SFREE(self->resolve); -#endif -#ifdef HAVE_CURL_7_20_0_OPTS - SFREE(self->mail_rcpt); -#endif -#ifdef HAVE_CURLOPT_CONNECT_TO - SFREE(self->connect_to); -#endif -#undef SFREE res = util_curl_init(self); if (res < 0) { Py_DECREF(self); /* this also closes self->handle */ @@ -441,6 +756,7 @@ PYCURL_INTERNAL PyMethodDef curlobject_methods[] = { {"setopt_string", (PyCFunction)do_curl_setopt_string, METH_VARARGS, curl_setopt_string_doc}, {"unsetopt", (PyCFunction)do_curl_unsetopt, METH_VARARGS, curl_unsetopt_doc}, {"reset", (PyCFunction)do_curl_reset, METH_NOARGS, curl_reset_doc}, + {"duphandle", (PyCFunction)do_curl_duphandle, METH_NOARGS, curl_duphandle_doc}, #if defined(HAVE_CURL_OPENSSL) {"set_ca_certs", (PyCFunction)do_curl_set_ca_certs, METH_VARARGS, curl_set_ca_certs_doc}, #endif diff --git a/src/easyopt.c b/src/easyopt.c index ba4108c..e3da86b 100644 --- a/src/easyopt.c +++ b/src/easyopt.c @@ -46,6 +46,12 @@ util_curl_unsetopt(CurlObject *self, int option) #define SETOPT2(o,x) \ if ((res = curl_easy_setopt(self->handle, (o), (x))) != CURLE_OK) goto error #define SETOPT(x) SETOPT2((CURLoption)option, (x)) +#define CLEAR_OBJECT(object_option, object_field) \ + case object_option: \ + if ((res = curl_easy_setopt(self->handle, object_option, NULL)) != CURLE_OK) \ + goto error; \ + Py_CLEAR(object_field); \ + break #define CLEAR_CALLBACK(callback_option, data_option, callback_field) \ case callback_option: \ if ((res = curl_easy_setopt(self->handle, callback_option, NULL)) != CURLE_OK) \ @@ -65,13 +71,6 @@ util_curl_unsetopt(CurlObject *self, int option) Py_XDECREF(self->share); self->share = NULL; break; - case CURLOPT_HTTPPOST: - SETOPT((void *) 0); - curl_formfree(self->httppost); - util_curl_xdecref(self, PYCURL_MEMGROUP_HTTPPOST, self->handle); - self->httppost = NULL; - /* FIXME: what about data->set.httpreq ?? */ - break; case CURLOPT_INFILESIZE: SETOPT((long) -1); break; @@ -108,10 +107,6 @@ util_curl_unsetopt(CurlObject *self, int option) case CURLOPT_SERVICE_NAME: case CURLOPT_PROXY_SERVICE_NAME: #endif - case CURLOPT_HTTPHEADER: -#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) - case CURLOPT_PROXYHEADER: -#endif #if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 52, 0) case CURLOPT_PROXY_CAPATH: case CURLOPT_PROXY_CAINFO: @@ -130,6 +125,27 @@ util_curl_unsetopt(CurlObject *self, int option) break; #endif + CLEAR_OBJECT(CURLOPT_HTTPHEADER, self->httpheader); +#if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) + CLEAR_OBJECT(CURLOPT_PROXYHEADER, self->proxyheader); +#endif + CLEAR_OBJECT(CURLOPT_HTTP200ALIASES, self->http200aliases); + CLEAR_OBJECT(CURLOPT_QUOTE, self->quote); + CLEAR_OBJECT(CURLOPT_POSTQUOTE, self->postquote); + CLEAR_OBJECT(CURLOPT_PREQUOTE, self->prequote); + CLEAR_OBJECT(CURLOPT_TELNETOPTIONS, self->telnetoptions); +#ifdef HAVE_CURLOPT_RESOLVE + CLEAR_OBJECT(CURLOPT_RESOLVE, self->resolve); +#endif +#ifdef HAVE_CURL_7_20_0_OPTS + CLEAR_OBJECT(CURLOPT_MAIL_RCPT, self->mail_rcpt); +#endif +#ifdef HAVE_CURLOPT_CONNECT_TO + CLEAR_OBJECT(CURLOPT_CONNECT_TO, self->connect_to); +#endif + /* FIXME: what about data->set.httpreq ?? */ + CLEAR_OBJECT(CURLOPT_HTTPPOST, self->httppost); + CLEAR_CALLBACK(CURLOPT_OPENSOCKETFUNCTION, CURLOPT_OPENSOCKETDATA, self->opensocket_cb); #if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 21, 7) CLEAR_CALLBACK(CURLOPT_CLOSESOCKETFUNCTION, CURLOPT_CLOSESOCKETDATA, self->closesocket_cb); @@ -690,16 +706,9 @@ do_curl_setopt_httppost(CurlObject *self, int option, int which, PyObject *obj) CURLERROR_SET_RETVAL(); goto error; } - /* Finally, free previously allocated httppost, ZAP any - * buffer references, and update */ - curl_formfree(self->httppost); - util_curl_xdecref(self, PYCURL_MEMGROUP_HTTPPOST, self->handle); - self->httppost = post; - - /* The previous list of INCed references was ZAPed above; save - * the new one so that we can clean it up on the next - * self->httppost free. */ - self->httppost_ref_list = ref_params; + /* Finally, decref previous httppost object and replace it with a + * new one. */ + util_curlhttppost_update(self, post, ref_params); Py_RETURN_NONE; @@ -713,48 +722,48 @@ error: static PyObject * do_curl_setopt_list(CurlObject *self, int option, int which, PyObject *obj) { - struct curl_slist **old_slist = NULL; + CurlSlistObject **old_slist_obj = NULL; struct curl_slist *slist = NULL; Py_ssize_t len; int res; switch (option) { case CURLOPT_HTTP200ALIASES: - old_slist = &self->http200aliases; + old_slist_obj = &self->http200aliases; break; case CURLOPT_HTTPHEADER: - old_slist = &self->httpheader; + old_slist_obj = &self->httpheader; break; #if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) case CURLOPT_PROXYHEADER: - old_slist = &self->proxyheader; + old_slist_obj = &self->proxyheader; break; #endif case CURLOPT_POSTQUOTE: - old_slist = &self->postquote; + old_slist_obj = &self->postquote; break; case CURLOPT_PREQUOTE: - old_slist = &self->prequote; + old_slist_obj = &self->prequote; break; case CURLOPT_QUOTE: - old_slist = &self->quote; + old_slist_obj = &self->quote; break; case CURLOPT_TELNETOPTIONS: - old_slist = &self->telnetoptions; + old_slist_obj = &self->telnetoptions; break; #ifdef HAVE_CURLOPT_RESOLVE case CURLOPT_RESOLVE: - old_slist = &self->resolve; + old_slist_obj = &self->resolve; break; #endif #ifdef HAVE_CURL_7_20_0_OPTS case CURLOPT_MAIL_RCPT: - old_slist = &self->mail_rcpt; + old_slist_obj = &self->mail_rcpt; break; #endif #ifdef HAVE_CURLOPT_CONNECT_TO case CURLOPT_CONNECT_TO: - old_slist = &self->connect_to; + old_slist_obj = &self->connect_to; break; #endif default: @@ -768,7 +777,7 @@ do_curl_setopt_list(CurlObject *self, int option, int which, PyObject *obj) Py_RETURN_NONE; /* Just to be sure we do not bug off here */ - assert(old_slist != NULL && slist == NULL); + assert(old_slist_obj != NULL && slist == NULL); /* Handle regular list operations on the other options */ slist = pycurl_list_or_tuple_to_slist(which, obj, len); @@ -781,9 +790,9 @@ do_curl_setopt_list(CurlObject *self, int option, int which, PyObject *obj) curl_slist_free_all(slist); CURLERROR_RETVAL(); } - /* Finally, free previously allocated list and update */ - curl_slist_free_all(*old_slist); - *old_slist = slist; + /* Finally, decref previous slist object and replace it with a + * new one. */ + util_curlslist_update(old_slist_obj, slist); Py_RETURN_NONE; } diff --git a/src/module.c b/src/module.c index b46a4fe..f75b84f 100644 --- a/src/module.c +++ b/src/module.c @@ -28,6 +28,8 @@ PYCURL_INTERNAL char *g_pycurl_useragent = NULL; /* Type objects */ PYCURL_INTERNAL PyObject *ErrorObject = NULL; PYCURL_INTERNAL PyTypeObject *p_Curl_Type = NULL; +PYCURL_INTERNAL PyTypeObject *p_CurlSlist_Type = NULL; +PYCURL_INTERNAL PyTypeObject *p_CurlHttppost_Type = NULL; PYCURL_INTERNAL PyTypeObject *p_CurlMulti_Type = NULL; PYCURL_INTERNAL PyTypeObject *p_CurlShare_Type = NULL; #ifdef HAVE_CURL_7_19_6_OPTS @@ -419,9 +421,13 @@ initpycurl(void) /* Initialize the type of the new type objects here; doing it here * is required for portability to Windows without requiring C++. */ p_Curl_Type = &Curl_Type; + p_CurlSlist_Type = &CurlSlist_Type; + p_CurlHttppost_Type = &CurlHttppost_Type; p_CurlMulti_Type = &CurlMulti_Type; p_CurlShare_Type = &CurlShare_Type; Py_SET_TYPE(&Curl_Type, &PyType_Type); + Py_SET_TYPE(&CurlSlist_Type, &PyType_Type); + Py_SET_TYPE(&CurlHttppost_Type, &PyType_Type); Py_SET_TYPE(&CurlMulti_Type, &PyType_Type); Py_SET_TYPE(&CurlShare_Type, &PyType_Type); @@ -429,12 +435,19 @@ initpycurl(void) if (PyType_Ready(&Curl_Type) < 0) goto error; + if (PyType_Ready(&CurlSlist_Type) < 0) + goto error; + + if (PyType_Ready(&CurlHttppost_Type) < 0) + goto error; + if (PyType_Ready(&CurlMulti_Type) < 0) goto error; if (PyType_Ready(&CurlShare_Type) < 0) goto error; + #if PY_MAJOR_VERSION >= 3 m = PyModule_Create(&curlmodule); if (m == NULL) diff --git a/src/pycurl.h b/src/pycurl.h index ae649b8..6f70a9a 100644 --- a/src/pycurl.h +++ b/src/pycurl.h @@ -334,6 +334,10 @@ PyText_Check(PyObject *o); PYCURL_INTERNAL PyObject * PyText_FromString_Ignore(const char *string); +/* Py_NewRef and Py_XNewRef - not part of Python's C API before 3.10 */ +static inline PyObject* my_Py_NewRef(PyObject *obj) { Py_INCREF(obj); return obj; } +static inline PyObject* my_Py_XNewRef(PyObject *obj) { Py_XINCREF(obj); return obj; } + struct CurlObject; PYCURL_INTERNAL void @@ -387,16 +391,30 @@ create_and_set_error_object(struct CurlObject *self, int code); #define PYCURL_MEMGROUP_POSTFIELDS 64 /* CA certs object */ #define PYCURL_MEMGROUP_CACERTS 128 +/* Curl slist objects */ +#define PYCURL_MEMGROUP_SLIST 256 #define PYCURL_MEMGROUP_EASY \ (PYCURL_MEMGROUP_CALLBACK | PYCURL_MEMGROUP_FILE | \ PYCURL_MEMGROUP_HTTPPOST | PYCURL_MEMGROUP_POSTFIELDS | \ - PYCURL_MEMGROUP_CACERTS) + PYCURL_MEMGROUP_CACERTS | PYCURL_MEMGROUP_SLIST) #define PYCURL_MEMGROUP_ALL \ (PYCURL_MEMGROUP_ATTRDICT | PYCURL_MEMGROUP_EASY | \ PYCURL_MEMGROUP_MULTI | PYCURL_MEMGROUP_SHARE) +typedef struct CurlSlistObject { + PyObject_HEAD + struct curl_slist *slist; +} CurlSlistObject; + +typedef struct CurlHttppostObject { + PyObject_HEAD + struct curl_httppost *httppost; + /* List of INC'ed references associated with httppost. */ + PyObject *reflist; +} CurlHttppostObject; + typedef struct CurlObject { PyObject_HEAD PyObject *dict; /* Python attributes dictionary */ @@ -408,26 +426,24 @@ typedef struct CurlObject { #endif struct CurlMultiObject *multi_stack; struct CurlShareObject *share; - struct curl_httppost *httppost; - /* List of INC'ed references associated with httppost. */ - PyObject *httppost_ref_list; - struct curl_slist *httpheader; + struct CurlHttppostObject *httppost; + struct CurlSlistObject *httpheader; #if LIBCURL_VERSION_NUM >= MAKE_LIBCURL_VERSION(7, 37, 0) - struct curl_slist *proxyheader; + struct CurlSlistObject *proxyheader; #endif - struct curl_slist *http200aliases; - struct curl_slist *quote; - struct curl_slist *postquote; - struct curl_slist *prequote; - struct curl_slist *telnetoptions; + struct CurlSlistObject *http200aliases; + struct CurlSlistObject *quote; + struct CurlSlistObject *postquote; + struct CurlSlistObject *prequote; + struct CurlSlistObject *telnetoptions; #ifdef HAVE_CURLOPT_RESOLVE - struct curl_slist *resolve; + struct CurlSlistObject *resolve; #endif #ifdef HAVE_CURL_7_20_0_OPTS - struct curl_slist *mail_rcpt; + struct CurlSlistObject *mail_rcpt; #endif #ifdef HAVE_CURLOPT_CONNECT_TO - struct curl_slist *connect_to; + struct CurlSlistObject *connect_to; #endif /* callbacks */ PyObject *w_cb; @@ -574,6 +590,11 @@ util_curl_xdecref(CurlObject *self, int flags, CURL *handle); PYCURL_INTERNAL PyObject * do_curl_setopt_filelike(CurlObject *self, int option, PyObject *obj); +PYCURL_INTERNAL void +util_curlslist_update(CurlSlistObject **old, struct curl_slist *slist); +PYCURL_INTERNAL void +util_curlhttppost_update(CurlObject *obj, struct curl_httppost *httppost, PyObject *reflist); + PYCURL_INTERNAL PyObject * do_curl_getinfo_raw(CurlObject *self, PyObject *args); #if PY_MAJOR_VERSION >= 3 @@ -635,11 +656,15 @@ ssl_ctx_callback(CURL *curl, void *ssl_ctx, void *ptr); #if !defined(PYCURL_SINGLE_FILE) /* Type objects */ extern PyTypeObject Curl_Type; +extern PyTypeObject CurlSlist_Type; +extern PyTypeObject CurlHttppost_Type; extern PyTypeObject CurlMulti_Type; extern PyTypeObject CurlShare_Type; extern PyObject *ErrorObject; extern PyTypeObject *p_Curl_Type; +extern PyTypeObject *p_CurlSlist_Type; +extern PyTypeObject *p_CurlHttppost_Type; extern PyTypeObject *p_CurlMulti_Type; extern PyTypeObject *p_CurlShare_Type; extern PyObject *khkey_type; diff --git a/tests/app.py b/tests/app.py index cf3fe75..62ff574 100644 --- a/tests/app.py +++ b/tests/app.py @@ -30,6 +30,7 @@ def forbidden(): def not_found(): return bottle.HTTPResponse('not found', 404) +@app.route('/postfields', method='get') @app.route('/postfields', method='post') def postfields(): return json.dumps(dict(bottle.request.forms)) @@ -88,7 +89,7 @@ def header(): # Thanks to bdarnell for the idea: https://github.com/pycurl/pycurl/issues/124 @app.route('/header_utf8') def header_utf8(): - header_value = bottle.request.headers[bottle.request.query['h']] + header_value = bottle.request.headers.get(bottle.request.query['h'], '' if py3 else b'') if py3: # header_value is a string, headers are decoded in latin1 header_value = header_value.encode('latin1').decode('utf8') diff --git a/tests/duphandle_test.py b/tests/duphandle_test.py new file mode 100644 index 0000000..fa30e80 --- /dev/null +++ b/tests/duphandle_test.py @@ -0,0 +1,144 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vi:ts=4:et + +from . import localhost +import pycurl +import unittest +import gc +import weakref +try: + import json +except ImportError: + import simplejson as json + +from . import appmanager +from . import util + +setup_module, teardown_module = appmanager.setup(('app', 8380)) + +class DuphandleTest(unittest.TestCase): + def setUp(self): + self.orig = util.DefaultCurl() + + def test_duphandle_attribute_dict(self): + self.orig.orig_attr = 'orig-value' + # attribute dict should be copied - the *object*, not the reference + dup = self.orig.duphandle() + assert dup.orig_attr == 'orig-value' + # cloned dict should be a separate object + dup.dup_attr = 'dup-value' + try: + self.orig.dup_attr == 'does not exist' + except AttributeError as error: + assert 'trying to obtain a non-existing attribute: dup_attr' in str(error.args) + else: + self.fail('should have raised AttributeError') + # dealloc self.orig - original dict is freed from memory + self.orig.close() + del self.orig + # cloned dict should still exist + assert dup.orig_attr == 'orig-value' + assert dup.dup_attr == 'dup-value' + dup.close() + + def slist_check(self, handle, value, persistance=True): + body = util.BytesIO() + handle.setopt(pycurl.WRITEFUNCTION, body.write) + handle.setopt(pycurl.URL, 'http://%s:8380/header_utf8?h=x-test-header' % localhost) + handle.perform() + result = body.getvalue().decode('utf-8') + assert (result == value) == persistance + + def slist_test(self, clear_func, *args): + # new slist object is created with ref count = 1 + self.orig.setopt(pycurl.HTTPHEADER, ['x-test-header: orig-slist']) + # ref is copied and object incref'ed + dup1 = self.orig.duphandle() + # slist object is decref'ed and ref set to null + clear_func(*args) + # null ref is copied - no effect + dup2 = self.orig.duphandle() + # check slist object persistance + self.slist_check(dup1, 'orig-slist', True) + self.slist_check(dup2, 'orig-slist', False) + # check overwriting - orig slist is decref'ed to 0 and finally deallocated + # util_curlslist_update() and util_curlslist_dealloc() + dup1.setopt(pycurl.HTTPHEADER, ['x-test-header: dup-slist']) + self.slist_check(dup1, 'dup-slist', True) + # cleanup + dup1.close() + dup2.close() + self.orig.close() + + def test_duphandle_slist_xdecref(self): + # util_curl_xdecref() + self.slist_test(self.orig.reset) + + def test_duphandle_slist_unsetopt(self): + # util_curl_unsetopt() + self.slist_test(self.orig.unsetopt, pycurl.HTTPHEADER) + + def httppost_check(self, handle, value, persistance=True): + body = util.BytesIO() + handle.setopt(pycurl.WRITEFUNCTION, body.write) + handle.setopt(pycurl.URL, 'http://%s:8380/postfields' % localhost) + handle.perform() + result = json.loads(body.getvalue()) + assert (result == value) == persistance + + def httppost_test(self, clear_func, *args): + self.orig.setopt(pycurl.HTTPPOST, [ + ('field', (pycurl.FORM_CONTENTS, 'orig-httppost')), + ]) + dup1 = self.orig.duphandle() + clear_func(*args) + dup2 = self.orig.duphandle() + self.httppost_check(dup1, {'field': 'orig-httppost'}, True) + self.httppost_check(dup2, {'field': 'orig-httppost'}, False) + # util_curlhttppost_update() and util_curlhttppost_dealloc() + dup1.setopt(pycurl.HTTPPOST, [ + ('field', (pycurl.FORM_CONTENTS, 'dup-httppost')), + ]) + self.httppost_check(dup1, {'field': 'dup-httppost'}, True) + dup1.close() + dup2.close() + self.orig.close() + + def test_duphandle_httppost_xdecref(self): + # util_curl_xdecref() + self.httppost_test(self.orig.reset) + + def test_duphandle_httppost_unsetopt(self): + # util_curl_unsetopt() + self.httppost_test(self.orig.unsetopt, pycurl.HTTPPOST) + + def test_duphandle_references(self): + body = util.BytesIO() + def callback(data): + body.write(data) + callback_ref = weakref.ref(callback) + # preliminary checks of gc and weakref working as expected + assert gc.get_referrers(callback) == [] + assert callback_ref() is not None + # setopt - callback ref is copied and callback incref'ed + self.orig.setopt(pycurl.WRITEFUNCTION, callback) + assert gc.get_referrers(callback) == [self.orig] + # duphandle - callback ref is copied and callback incref'ed + dup = self.orig.duphandle() + assert set(gc.get_referrers(callback)) == {self.orig, dup} + # dealloc self.orig and decref callback + self.orig.close() + del self.orig + assert gc.get_referrers(callback) == [dup] + # decref callback again - back to ref count = 1 + del callback + assert callback_ref() is not None + # check that callback object still exists and is invoked + dup.setopt(pycurl.URL, 'http://%s:8380/success' % localhost) + dup.perform() + result = body.getvalue().decode('utf-8') + assert result == 'success' + # final decref - callback is deallocated + dup.close() + assert callback_ref() is None |