Skip to content

Commit 73711a8

Browse files
ch3pjwhoefling
authored andcommitted
First cut of registering Python callbacks for xmlsec
This implementation uses a global linked-list structure to hold onto the Python callbacks and registers a single C wrapper callback with xmlsec to dispatch xmlsec's calling back to the appropriate Python function. This was the simplest way I could think to emulate dynamically wrapping the Python calls in C code, because C doesn't have closures. A potential downside is that the state is all very global. Perhaps, however, that's no worse than the very global set of callbacks that xmlsec holds onto itself, so long as we don't have any strange threading stuff going on. In order to accommodate client code potentially interleaving registrations of the default callbacks in between their own callbacks, the linked list structure can have nodes with NULL function pointers in it, such that when iterating through Python callbacks we can suspend our iteration and delegate back to the defaults at the appropriate point. As such, we always actually have the Python binding's C callbacks registered with xmlsec, and every time we get asked to register the default callbacks, we note a NULL function pointer and then re-register our C callbacks.
1 parent 8ed003c commit 73711a8

File tree

1 file changed

+242
-0
lines changed

1 file changed

+242
-0
lines changed

src/main.c

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <xmlsec/crypto.h>
1616
#include <xmlsec/errors.h>
1717
#include <xmlsec/base64.h>
18+
#include <xmlsec/io.h>
1819

1920
#define _PYXMLSEC_FREE_NONE 0
2021
#define _PYXMLSEC_FREE_XMLSEC 1
@@ -133,6 +134,229 @@ static PyObject* PyXmlSec_PyEnableDebugOutput(PyObject *self, PyObject* args, Py
133134
Py_RETURN_NONE;
134135
}
135136

137+
// NB: This whole thing assumes that the `xmlsec` callbacks are not re-entrant
138+
// (i.e. that xmlsec won't come across a link in the reference it's processing
139+
// and try to open that with these callbacks too).
140+
typedef struct CbList {
141+
PyObject* match_cb;
142+
PyObject* open_cb;
143+
PyObject* read_cb;
144+
PyObject* close_cb;
145+
struct CbList* next;
146+
} CbList;
147+
148+
static CbList* registered_callbacks = NULL;
149+
static CbList* rcb_tail = NULL;
150+
151+
static void RCBListAppend(CbList* cb_list_item) {
152+
if (registered_callbacks == NULL) {
153+
registered_callbacks = cb_list_item;
154+
} else {
155+
rcb_tail->next = cb_list_item;
156+
}
157+
rcb_tail = cb_list_item;
158+
}
159+
160+
static void RCBListClear() {
161+
CbList* cb_list_item = registered_callbacks;
162+
while (cb_list_item) {
163+
Py_XDECREF(cb_list_item->match_cb);
164+
Py_XDECREF(cb_list_item->open_cb);
165+
Py_XDECREF(cb_list_item->read_cb);
166+
Py_XDECREF(cb_list_item->close_cb);
167+
CbList* next = cb_list_item->next;
168+
free(cb_list_item);
169+
cb_list_item = next;
170+
}
171+
registered_callbacks = NULL;
172+
rcb_tail = NULL;
173+
}
174+
175+
// The currently executing set of Python callbacks:
176+
static CbList* cur_cb_list_item = NULL;
177+
178+
static int PyXmlSec_MatchCB(const char* filename) {
179+
if (!cur_cb_list_item) {
180+
cur_cb_list_item = registered_callbacks;
181+
}
182+
while (cur_cb_list_item && !cur_cb_list_item->match_cb) {
183+
// Spool past any default callback placeholders executed since we were
184+
// last called back:
185+
cur_cb_list_item = cur_cb_list_item->next;
186+
}
187+
PyGILState_STATE state = PyGILState_Ensure();
188+
PyObject* args = Py_BuildValue("(y)", filename);
189+
while (cur_cb_list_item && cur_cb_list_item->match_cb) {
190+
PyObject* result = PyObject_CallObject(cur_cb_list_item->match_cb, args);
191+
if (result && PyObject_IsTrue(result)) {
192+
Py_DECREF(result);
193+
Py_DECREF(args);
194+
PyGILState_Release(state);
195+
return 1;
196+
}
197+
cur_cb_list_item = cur_cb_list_item->next;
198+
}
199+
// FIXME: why does having this decref of args cause a segfault?!
200+
Py_DECREF(args);
201+
PyGILState_Release(state);
202+
return 0;
203+
}
204+
205+
static void* PyXmlSec_OpenCB(const char* filename) {
206+
PyGILState_STATE state = PyGILState_Ensure();
207+
208+
// NB: Assumes the match callback left the current callback list item in the
209+
// right place:
210+
PyObject* args = Py_BuildValue("(y)", filename);
211+
PyObject* result = PyObject_CallObject(cur_cb_list_item->open_cb, args);
212+
Py_DECREF(args);
213+
214+
PyGILState_Release(state);
215+
return result;
216+
}
217+
218+
static int PyXmlSec_ReadCB(void* context, char* buffer, int len) {
219+
PyGILState_STATE state = PyGILState_Ensure();
220+
221+
// NB: Assumes the match callback left the current callback list item in the
222+
// right place:
223+
PyObject* py_buffer = PyMemoryView_FromMemory(buffer, (Py_ssize_t) len, PyBUF_WRITE);
224+
PyObject* args = Py_BuildValue("(OO)", context, py_buffer);
225+
PyObject* py_bytes_read = PyObject_CallObject(cur_cb_list_item->read_cb, args);
226+
Py_DECREF(args);
227+
Py_DECREF(py_buffer);
228+
int result;
229+
if (py_bytes_read && PyLong_Check(py_bytes_read)) {
230+
result = (int)PyLong_AsLong(py_bytes_read);
231+
Py_DECREF(py_bytes_read);
232+
} else {
233+
result = EOF;
234+
}
235+
236+
PyGILState_Release(state);
237+
return result;
238+
}
239+
240+
static int PyXmlSec_CloseCB(void* context) {
241+
PyGILState_STATE state = PyGILState_Ensure();
242+
243+
PyObject* args = Py_BuildValue("(O)", context);
244+
PyObject* result = PyObject_CallObject(cur_cb_list_item->close_cb, args);
245+
Py_DECREF(args);
246+
Py_DECREF(context);
247+
Py_DECREF(result);
248+
249+
PyGILState_Release(state);
250+
// We reset `cur_cb_list_item` because we've finished processing the set of
251+
// callbacks that was matched
252+
cur_cb_list_item = NULL;
253+
return 0;
254+
}
255+
256+
static char PyXmlSec_PyIOCleanupCallbacks__doc__[] = \
257+
"Unregister globally all sets of IO callbacks from xmlsec.";
258+
static PyObject* PyXmlSec_PyIOCleanupCallbacks(PyObject *self) {
259+
xmlSecIOCleanupCallbacks();
260+
// We always have callbacks registered to delegate to any Python callbacks
261+
// we have registered within these bindings:
262+
if (xmlSecIORegisterCallbacks(
263+
PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB,
264+
PyXmlSec_CloseCB) < 0) {
265+
return NULL;
266+
};
267+
RCBListClear();
268+
Py_RETURN_NONE;
269+
}
270+
271+
static char PyXmlSec_PyIORegisterDefaultCallbacks__doc__[] = \
272+
"Register globally xmlsec's own default set of IO callbacks.";
273+
static PyObject* PyXmlSec_PyIORegisterDefaultCallbacks(PyObject *self) {
274+
if (xmlSecIORegisterDefaultCallbacks() < 0) {
275+
return NULL;
276+
}
277+
// We place a nulled item on the callback list to represent whenever the
278+
// default callbacks are going to be invoked:
279+
CbList* cb_list_item = malloc(sizeof(CbList));
280+
if (cb_list_item == NULL) {
281+
return NULL;
282+
}
283+
cb_list_item->match_cb = NULL;
284+
cb_list_item->open_cb = NULL;
285+
cb_list_item->read_cb = NULL;
286+
cb_list_item->close_cb = NULL;
287+
cb_list_item->next = NULL;
288+
RCBListAppend(cb_list_item);
289+
// We need to make sure we can continue trying to match futher Python
290+
// callbacks if the default callback doesn't match:
291+
if (xmlSecIORegisterCallbacks(
292+
PyXmlSec_MatchCB, PyXmlSec_OpenCB, PyXmlSec_ReadCB,
293+
PyXmlSec_CloseCB) < 0) {
294+
return NULL;
295+
};
296+
Py_RETURN_NONE;
297+
}
298+
299+
static char PyXmlSec_PyIORegisterCallbacks__doc__[] = \
300+
"Register globally a custom set of IO callbacks with xmlsec.\n\n"
301+
":param callable input_match_callback: A callable that takes a filename `bytestring` and "
302+
"returns a boolean as to whether the other callbacks in this set can handle that name.\n"
303+
":param callable input_open_callback: A callable that takes a filename and returns some "
304+
"context object (e.g. a file object) that the remaining callables in this set will be passed "
305+
"during handling.\n"
306+
// FIXME: How do we handle failures in ^^ (e.g. can't find the file)?
307+
":param callable input_read_callback: A callable that that takes the context object from the "
308+
"open callback and a buffer, and should fill the buffer with data (e.g. BytesIO.readinto()). "
309+
"xmlsec will call this function several times until there is no more data returned.\n"
310+
":param callable input_close_callback: A callable that takes the context object from the "
311+
"open callback and can do any resource cleanup necessary.\n"
312+
;
313+
static PyObject* PyXmlSec_PyIORegisterCallbacks(PyObject *self, PyObject *args, PyObject *kwargs) {
314+
static char *kwlist[] = {
315+
"input_match_callback",
316+
"input_open_callback",
317+
"input_read_callback",
318+
"input_close_callback",
319+
NULL
320+
};
321+
CbList* cb_list_item = malloc(sizeof(CbList));
322+
if (cb_list_item == NULL) {
323+
return NULL;
324+
}
325+
if (!PyArg_ParseTupleAndKeywords(
326+
args, kwargs, "OOOO:register_callbacks", kwlist,
327+
&cb_list_item->match_cb, &cb_list_item->open_cb, &cb_list_item->read_cb,
328+
&cb_list_item->close_cb)) {
329+
free(cb_list_item);
330+
return NULL;
331+
}
332+
if (!PyCallable_Check(cb_list_item->match_cb)) {
333+
PyErr_SetString(PyExc_TypeError, "input_match_callback must be a callable");
334+
return NULL;
335+
}
336+
if (!PyCallable_Check(cb_list_item->open_cb)) {
337+
PyErr_SetString(PyExc_TypeError, "input_open_callback must be a callable");
338+
return NULL;
339+
}
340+
if (!PyCallable_Check(cb_list_item->read_cb)) {
341+
PyErr_SetString(PyExc_TypeError, "input_read_callback must be a callable");
342+
return NULL;
343+
}
344+
if (!PyCallable_Check(cb_list_item->close_cb)) {
345+
PyErr_SetString(PyExc_TypeError, "input_close_callback must be a callable");
346+
return NULL;
347+
}
348+
Py_INCREF(cb_list_item->match_cb);
349+
Py_INCREF(cb_list_item->open_cb);
350+
Py_INCREF(cb_list_item->read_cb);
351+
Py_INCREF(cb_list_item->close_cb);
352+
cb_list_item->next = NULL;
353+
RCBListAppend(cb_list_item);
354+
// NB: We don't need to register the callbacks with `xmlsec` here, because
355+
// we've already registered our helper functions that will trawl through our
356+
// list of callbacks.
357+
Py_RETURN_NONE;
358+
}
359+
136360
static char PyXmlSec_PyBase64DefaultLineSize__doc__[] = \
137361
"base64_default_line_size(size = None)\n"
138362
"Configures the default maximum columns size for base64 encoding.\n\n"
@@ -181,6 +405,24 @@ static PyMethodDef PyXmlSec_MainMethods[] = {
181405
METH_VARARGS|METH_KEYWORDS,
182406
PyXmlSec_PyEnableDebugOutput__doc__
183407
},
408+
{
409+
"cleanup_callbacks",
410+
(PyCFunction)PyXmlSec_PyIOCleanupCallbacks,
411+
METH_NOARGS,
412+
PyXmlSec_PyIOCleanupCallbacks__doc__
413+
},
414+
{
415+
"register_default_callbacks",
416+
(PyCFunction)PyXmlSec_PyIORegisterDefaultCallbacks,
417+
METH_NOARGS,
418+
PyXmlSec_PyIORegisterDefaultCallbacks__doc__
419+
},
420+
{
421+
"register_callbacks",
422+
(PyCFunction)PyXmlSec_PyIORegisterCallbacks,
423+
METH_VARARGS|METH_KEYWORDS,
424+
PyXmlSec_PyIORegisterCallbacks__doc__
425+
},
184426
{
185427
"base64_default_line_size",
186428
(PyCFunction)PyXmlSec_PyBase64DefaultLineSize,

0 commit comments

Comments
 (0)