chiark / gitweb /
use libinn logging where applicable - debugged
[inn-innduct.git] / innd / python.c
1 /*  $Id: python.c 7891 2008-06-22 09:59:11Z iulius $
2 **
3 **  python.c: Embed Python in the style of innd's TCL and Perl stuff.
4 ** 
5 **  Written by G.J. Andruk <meowing@banet.net> patterned after
6 **  TCL/Perl work by Bob Heiney and Christophe Wolfhugel and a whole
7 **  bunch of other people mentioned in the docs and sources for the
8 **  other filters.
9 **
10 **  The astute reader may notice the commission of blatant atrocities
11 **  against Python's OO model here.  Don't tell Guido.
12 **
13 **  A quick note regarding Python exceptions:  functions like
14 **      PyObject_GetAttrString(PyObject *o, const char *attr_name)
15 **  raise an exception when they fail, even though they return NULL.
16 **  And as exceptions accumulate from caller to caller and so on,
17 **  it generates weird issues with Python scripts afterwards.  So such
18 **  uses should be checked before.  For instance with:
19 **      PyObject_HasAttrString(PyObject *o, const char *attr_name).
20 */
21
22 #include "config.h"
23 #include "clibrary.h"
24
25 #include "inn/innconf.h"
26 #include "innd.h"
27
28
29 #if defined(DO_PYTHON)
30
31 #include "Python.h"
32
33
34 bool            PythonFilterActive;
35 char            *filterPath;    /* this gets set in art.c */
36 PyObject        *PYFilterObject = NULL;
37 PyObject        *PYFilterModule = NULL;
38
39 /* article filter bits and pieces */
40 PyObject        *PYheaders = NULL;
41 PyObject        **PYheaditem;
42 PyObject        **PYheadkey;
43 PyObject        *PYpathkey, *PYlineskey, *PYbodykey;
44
45 /* external functions */
46 PyObject        *msgid_method = NULL;
47 PyObject        *art_method = NULL;
48 PyObject        *mode_method = NULL;
49 PyObject        *pre_reload_method = NULL;
50 PyObject        *close_method = NULL;
51
52
53
54 /*
55 **  Turn filtering on or off.
56 */
57 void
58 PYfilter(value)
59     bool value;
60 {
61     PythonFilterActive = value;
62     syslog(L_NOTICE, "%s Python filtering %s", LogName,
63            PythonFilterActive ? "enabled" : "disabled");
64 }
65
66
67
68 /*
69 **  Front end for PYfilter()
70 */
71 const char *
72 PYcontrol(char **av)
73 {
74     char        *p;
75     extern bool PythonFilterActive;
76
77     switch (av[0][0]) {
78     default:
79         return "1 Bad flag";
80     case 'y':
81         if (PythonFilterActive)
82             return "1 Python filter already enabled";
83         else if (PYFilterObject == NULL)
84             return "1 Python filter not defined" ;
85         PYfilter(true);
86         break;
87     case 'n':
88         if (!PythonFilterActive)
89             return "1 Python filter already disabled";
90         PYfilter(false);
91         break;
92     }
93     return NULL;
94 }
95
96
97
98
99 /*
100 **  Reject articles we don't like.
101 */
102 char *
103 PYartfilter(const ARTDATA *data, char *artBody, long artLen, int lines)
104 {
105     const ARTHEADER *hp;
106     const HDRCONTENT *hc = data->HdrContent;
107     int         hdrnum;
108     int         i;
109     char        *p, save;
110     static char buf[256];
111     PyObject    *result;
112
113     if (!PythonFilterActive || PYFilterObject == NULL || art_method == NULL)
114         return NULL;
115
116     /* Add headers to the dictionary... */
117     hdrnum = 0;
118     for (i = 0 ; i < MAX_ARTHEADER ; i++) {
119         if (HDR_FOUND(i)) {
120             hp = &ARTheaders[i];
121             PYheaditem[hdrnum] = PyBuffer_FromMemory(HDR(i), HDR_LEN(i));
122         } else
123             PYheaditem[hdrnum] = Py_None;
124         PyDict_SetItem(PYheaders, PYheadkey[hdrnum], PYheaditem[hdrnum]);
125         hdrnum++;
126     }
127
128     /* ...then the body... */
129     if (artLen && artBody != NULL)
130         PYheaditem[hdrnum] = PyBuffer_FromMemory(artBody, --artLen);
131     else
132         PYheaditem[hdrnum] = Py_None;
133     PyDict_SetItem(PYheaders, PYbodykey, PYheaditem[hdrnum++]);
134
135     /* ...and finally, the line count. */
136     PYheaditem[hdrnum] = PyInt_FromLong((long) lines);
137     PyDict_SetItem(PYheaders, PYlineskey, PYheaditem[hdrnum++]);
138
139     /* Now see if the filter likes it. */
140     result = PyObject_CallFunction(art_method, "O", PYheaders);
141     if ((result != NULL) && PyObject_IsTrue(result))
142         strlcpy(buf, PyString_AS_STRING(result), sizeof(buf));
143     else
144         *buf = '\0';
145     Py_XDECREF(result);
146
147     /* Clean up after ourselves */
148     PyDict_Clear(PYheaders);
149     for (i = 0; i < hdrnum; i++)
150         if (PYheaditem[i] != Py_None)
151             Py_DECREF(PYheaditem[i]);
152
153     if (*buf != '\0') 
154         return buf;
155     return NULL;
156 }
157
158
159
160 /*
161 **  Refuse message IDs offered thru CHECK or IHAVE that we don't like.
162 */
163 char *
164 PYmidfilter(messageID, msglen)
165     char *messageID;
166     int msglen;
167 {
168     static char         buf[256];
169     PyObject            *result;
170
171     if (!PythonFilterActive || PYFilterObject == NULL || msgid_method == NULL)
172         return NULL;
173
174     result = PyObject_CallFunction(msgid_method, "s#", messageID, msglen);
175     if ((result != NULL) && PyObject_IsTrue(result))
176         strlcpy(buf, PyString_AS_STRING(result), sizeof(buf));
177     else
178         *buf = '\0';
179     Py_XDECREF(result);
180
181     if (*buf != '\0') 
182         return buf;
183     return NULL;
184 }
185
186
187
188 /*
189 **  Tell the external module about innd's state.
190 */
191 void
192 PYmode(Mode, NewMode, reason)
193     OPERATINGMODE Mode, NewMode;
194     char *reason;
195 {
196     PyObject    *result;
197     char        oldmode[10], newmode[10];
198
199     if (!PythonFilterActive || PYFilterObject == NULL || mode_method == NULL)
200         return;
201
202     switch (Mode) {
203     default:            strlcpy(oldmode, "unknown", 10);        break;
204     case OMrunning:     strlcpy(oldmode, "running", 10);        break;
205     case OMpaused:      strlcpy(oldmode, "paused", 10);         break;
206     case OMthrottled:   strlcpy(oldmode, "throttled", 10);      break;
207     }
208
209     switch (NewMode) {
210     default:            strlcpy(newmode, "unknown", 10);        break;
211     case OMrunning:     strlcpy(newmode, "running", 10);        break;
212     case OMpaused:      strlcpy(newmode, "paused", 10);         break;
213     case OMthrottled:   strlcpy(newmode, "throttled", 10);      break;
214     }
215
216     result = PyObject_CallFunction(mode_method, "sss",
217                                    oldmode, newmode, reason);
218     Py_XDECREF(result);
219 }
220
221
222
223 /*
224 **  Called by the external module so it can register itself with innd.
225 */
226 static PyObject *
227 PY_set_filter_hook(dummy, args)
228     PyObject *dummy, *args;
229 {
230     PyObject    *result = NULL;
231     PyObject    *temp;
232
233     if (PyArg_ParseTuple(args, "O:set_filter_hook", &temp)) {
234         Py_XINCREF(temp);
235         Py_XDECREF(PYFilterObject);
236         PYFilterObject = temp;
237         Py_INCREF(Py_None);
238         result = Py_None;
239     }
240     return result;
241 }
242
243
244
245 /*
246 **  Allow external module to ask innd if an ID is in history.
247 */
248 static PyObject *
249 PY_havehist(self, args)
250     PyObject *self, *args;
251 {
252     char        *msgid;
253     int         msgidlen;
254
255     if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
256         return NULL;
257
258     if (HIScheck(History, msgid))
259         return PyInt_FromLong(1);
260     return PyInt_FromLong(0);
261 }
262
263
264
265 /*
266 **  Allow external module to locally delete an article.
267 */
268 static PyObject *
269 PY_cancel(self, args)
270     PyObject *self, *args;
271 {
272     char        *msgid;
273     int         msgidlen;
274     char        *parambuf[2];
275
276     if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
277         return NULL;
278
279     parambuf[0]= msgid;
280     parambuf[1]= 0;
281
282     if (!CCcancel(parambuf))
283         return PyInt_FromLong(1);
284     return PyInt_FromLong(0);
285 }
286
287
288
289 /*
290 **  Stuff an ID into history so that it will be refused later.
291 */
292 static PyObject *
293 PY_addhist(self, args)
294     PyObject *self, *args;
295 {
296     char        *msgid;
297     int         msgidlen;
298     char        *articlepaths = "";
299     char        tbuff[12];
300     char        *parambuf[6];
301
302     if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
303         return NULL;
304
305     snprintf(tbuff, sizeof(tbuff), "%d", time(NULL));
306
307     parambuf[0] = msgid;
308     parambuf[1] = parambuf[2] = parambuf[3] = tbuff;
309     parambuf[4] = articlepaths;
310     parambuf[5] = 0;
311
312     if (!CCaddhist(parambuf))
313         return PyInt_FromLong(1);
314     return PyInt_FromLong(0);
315 }
316
317
318
319 /*
320 **  Get a newsgroup's status flag (j, m, n, x, y, =other.group)
321 */
322 static PyObject *
323 PY_newsgroup(self, args)
324     PyObject *self, *args;
325 {
326     char        *newsgroup;
327     int         nglen;
328     NEWSGROUP   *ngp;
329     char        *end;
330     char        *rest;
331     int         size;
332
333     if (!PyArg_ParseTuple(args, "s#", &newsgroup, &nglen))
334         return NULL;
335
336     ngp = NGfind(newsgroup);
337     if (ngp == NULL)
338         return PyString_FromStringAndSize(NULL, 0);
339
340     /* ngp->Rest is newline-terminated; find the end. */
341     end = strchr(ngp->Rest, '\n');
342     if (end == NULL)
343         size = strlen(ngp->Rest);
344     else
345         size = end - ngp->Rest;
346
347     /* If an alias is longer than this, active is probably broken. */
348     if (size > MAXHEADERSIZE) {
349         syslog(L_ERROR, "too-long flag field in active for %s", newsgroup);
350         size = MAXHEADERSIZE;
351     }
352
353     return PyString_FromStringAndSize(ngp->Rest, size);
354 }
355
356
357
358 /*
359 **  Return an article header to the external module as a string.  We
360 **  don't use a buffer object here because that would make it harder,
361 **  for example, to compare two on-spool articles.
362 */
363 static PyObject *
364 PY_head(self, args)
365     PyObject *self, *args;
366 {
367     char        *msgid;
368     int         msgidlen;
369     char        *p;
370     TOKEN       token;
371     ARTHANDLE   *art;
372     PyObject    *header;
373     int         headerlen;
374
375     if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
376         return NULL;
377
378     if (! HISlookup(History, msgid, NULL, NULL, NULL, &token))
379         return Py_BuildValue("s", "");  
380     if ((art = SMretrieve(token, RETR_HEAD)) == NULL)
381         return Py_BuildValue("s", "");  
382     p = FromWireFmt(art->data, art->len, &headerlen);
383     SMfreearticle(art);
384     header = PyString_FromStringAndSize(p, headerlen);
385     free(p);
386
387     return header;
388 }
389
390
391
392 /*
393 **  Return a whole article to the external module as a string.
394 */
395 static PyObject *
396 PY_article(self, args)
397     PyObject *self, *args;
398 {
399     char        *msgid;
400     int         msgidlen;
401     char        *p;
402     TOKEN       token;
403     ARTHANDLE   *arth;
404     PyObject    *art;
405     int         artlen;
406
407     if (!PyArg_ParseTuple(args, "s#", &msgid, &msgidlen))
408         return NULL;
409
410     if (! HISlookup(History, msgid, NULL, NULL, NULL, &token))
411         return Py_BuildValue("s", "");
412     if ((arth = SMretrieve(token, RETR_ALL)) == NULL)
413         return Py_BuildValue("s", "");  
414     p = FromWireFmt(arth->data, arth->len, &artlen);
415     SMfreearticle(arth);
416     art = PyString_FromStringAndSize(p, artlen);
417     free(p);
418
419     return art;
420 }
421
422
423
424 /*
425 **  Python's syslog module isn't compiled in by default.  It's easier
426 **  to do it this way, and the switch block looks pretty in a color
427 **  editor).
428 */
429 static PyObject *
430 PY_syslog(self, args)
431     PyObject *self, *args;
432 {
433     char        *loglevel;
434     int         levellen;
435     char        *logmsg;
436     int         msglen;
437     int         priority;
438
439     if (!PyArg_ParseTuple(args, "s#s#",
440                           &loglevel, &levellen, &logmsg, &msglen))
441         return NULL;
442
443     switch (*loglevel) {
444     default:            priority = LOG_NOTICE ;
445     case 'd': case 'D': priority = LOG_DEBUG ;          break;
446     case 'i': case 'I': priority = LOG_INFO ;           break;
447     case 'n': case 'N': priority = LOG_NOTICE ;         break;
448     case 'w': case 'W': priority = LOG_WARNING ;        break;
449     case 'e': case 'E': priority = LOG_ERR ;            break;
450     case 'c': case 'C': priority = LOG_CRIT ;           break;
451     case 'a': case 'A': priority = LOG_ALERT ;          break;
452     }
453
454     syslog(priority, "python: %s", logmsg);
455
456     Py_INCREF(Py_None);
457     return Py_None;
458 }
459
460
461
462 /*
463 **  Compute a hash digest for a string.
464 */
465 static PyObject *
466 PY_hashstring(self, args)
467     PyObject *self, *args;
468 {
469     char        *instring, *wpos, *p, *q;
470     char        *workstring = NULL;
471     int         insize, worksize, newsize, i, wasspace;
472     int         lines = 0;
473     HASH        myhash;
474
475     if (!PyArg_ParseTuple(args, "s#|i", &instring, &insize, &lines))
476         return NULL;
477
478     /* If a linecount is provided, munge before hashing. */
479     if (lines > 0) {
480         worksize = insize;
481
482         /* chop leading whitespace */
483         for (p=instring ; worksize>0 && isspace(*p) ; p++) {
484             if (*p == '\n')
485                 lines--;
486             worksize--;
487         }
488         wpos = p;
489
490         /* and trailing */
491         for (p=&wpos[worksize] ; worksize>0 && isspace(*p) ; p--) {
492             if (*p == '\n')
493                 lines--;
494             worksize--;
495         }
496
497         /* chop last 3 lines if we have >= 5.  From above chop the
498          * last line has no CR so we use 1 less here. */
499         if (lines >= 4) {
500             for (i=0, p=wpos+worksize ; i<2 ; p--)
501                 if (*p == '\n')
502                     i++;
503             worksize = p - wpos;
504         }
505
506         /* Compress out multiple whitespace in the trimmed string.  We
507          * do a copy because this is probably an original art
508          * buffer. */
509         workstring =  memcpy(xmalloc(worksize), wpos, worksize);
510         newsize = wasspace = 0;
511         p = wpos;
512         q = workstring;
513         for (i=0 ; i<worksize ; i++) {
514             if (isspace(*p)) {
515                 if (!wasspace)
516                     *q++ = ' ';
517                 wasspace = 1;
518             }
519             else {
520                 *q++ = tolower(*p);
521                 wasspace = 0;
522             }
523             p++;
524         }
525         worksize = q - workstring;
526         myhash = Hash(workstring, worksize);
527         free(workstring);
528     }
529     else
530         myhash = Hash(instring, insize);
531
532     return PyString_FromStringAndSize((const char *)&myhash, sizeof(myhash));
533 }
534
535
536
537 /*
538 **  Make the internal INN module's functions visible to Python.
539 */
540 static PyMethodDef INNPyMethods[] = {
541     {"set_filter_hook", PY_set_filter_hook,     METH_VARARGS},
542     {"havehist",        PY_havehist,            METH_VARARGS},
543     {"addhist",         PY_addhist,             METH_VARARGS},
544     {"cancel",          PY_cancel,              METH_VARARGS},
545     {"newsgroup",       PY_newsgroup,           METH_VARARGS},
546     {"head",            PY_head,                METH_VARARGS},
547     {"article",         PY_article,             METH_VARARGS},
548     {"syslog",          PY_syslog,              METH_VARARGS},
549     {"hashstring",      PY_hashstring,          METH_VARARGS},
550     {NULL,              NULL}
551 };
552
553
554
555 /*
556 **  This runs when innd shuts down.
557 */
558 void
559 PYclose(void)
560 {
561     PyObject    *result;
562
563     if (close_method != NULL) {
564         result = PyObject_CallFunction(close_method, NULL);
565         Py_XDECREF(result);
566     }
567 }
568
569
570
571 /*
572 **  Check that a method exists and is callable.  Set a pointer to
573 **  the corresponding PyObject, or NULL if not found.
574 */
575 void
576 PYdefonemethod(methptr, methname)
577     PyObject    **methptr;
578     char        *methname;
579 {
580     Py_XDECREF(*methptr);
581
582     /* We check with HasAttrString() the existence of the method because
583      * otherwise, in case it does not exist, an exception is raised by Python,
584      * although the result of the function is NULL. */
585     if (PyObject_HasAttrString(PYFilterObject, (char *) methname) == 1) {
586         /* Get a pointer to given method. */
587         *methptr = PyObject_GetAttrString(PYFilterObject, (char *) methname);
588     } else {
589         *methptr = NULL;
590     }
591                 
592     if (*methptr == NULL)
593         syslog(L_NOTICE, "python method %s not found", methname);
594     else if (PyCallable_Check(*methptr) == 0) {
595         syslog(L_ERROR, "python object %s found but not a function", methname);
596         Py_DECREF(*methptr);
597         *methptr = NULL;
598     }
599 }
600
601
602
603 /*
604 **  Look up the filter methods, so we will know what's available when
605 **  innd wants to call them.
606 */
607 void
608 PYdefmethods(void)
609 {
610     PYdefonemethod(&msgid_method, "filter_messageid");
611     PYdefonemethod(&art_method, "filter_art");
612     PYdefonemethod(&mode_method, "filter_mode");
613     PYdefonemethod(&pre_reload_method, "filter_before_reload");
614     PYdefonemethod(&close_method, "filter_close");
615 }
616
617
618
619 /*
620 **  Used by "ctlinnd reload filter.python 'reason'".
621 */
622 int
623 PYreadfilter(void)
624 {
625     PyObject    *newmodule = NULL;
626     PyObject    *result;
627
628     if (!Py_IsInitialized()) {
629         syslog(L_ERROR, "python is not initialized");
630         return 0;
631     }
632
633     /* If there is a filter running, let it clean up first. */
634     if (pre_reload_method != NULL) {
635         result = PyObject_CallFunction(pre_reload_method, NULL);
636         Py_XDECREF(result);
637     }
638
639     /* We need to reimport the module before reloading it because otherwise,
640      * it might not be taken into account by Python.
641      * See Python API documentation:
642      *     If a module is syntactically correct but its initialization fails,
643      *     the first import statement for it does not bind its name locally,
644      *     but does store a (partially initialized) module object in
645      *     sys.modules.  To reload the module, you must first import it again
646      *     (this will bind the name to the partially initialized module object)
647      *     before you can reload() it.
648      */
649     PYFilterModule = PyImport_ImportModule((char *) _PATH_PYTHON_STARTUP_M);
650     if (PYFilterModule == NULL) {
651         syslog(L_ERROR, "failed to reimport external python module");
652     }
653
654     if ((newmodule = PyImport_ReloadModule(PYFilterModule)) == NULL) {
655         syslog(L_ERROR, "cant reload python filter module");
656         PYfilter(false);
657         return 0;
658     }
659
660     Py_XDECREF(PYFilterModule);
661     PYFilterModule = newmodule;
662
663     if (PYFilterObject == NULL) {
664         syslog(L_ERROR, "python reload error, filter object not defined");
665         PYfilter(false);
666         return 0;
667     }
668
669     PYfilter(true);
670     PYdefmethods();
671
672     return 1;
673 }
674
675
676
677 /*
678 **  Called when innd first starts -- this gets the filters hooked in.
679 */
680 void
681 PYsetup(void)
682 {
683     const ARTHEADER *hp;
684     int         hdrindex;
685     size_t hdrcount;
686
687     setenv("PYTHONPATH", innconf->pathfilter, 1);
688     Py_Initialize();
689
690     /* It makes Python sad when its stdout and stderr are closed. */
691     if ((fileno(stdout) == -1) || (fileno(stderr) == -1))
692         PyRun_SimpleString
693             ("import sys; sys.stdout=sys.stderr=open('/dev/null', 'a')");
694
695     if (!Py_IsInitialized ()) {
696         syslog(L_ERROR, "python interpreter NOT initialized");
697         return;
698     }
699     syslog(L_NOTICE, "python interpreter initialized OK");
700
701     Py_InitModule("INN", INNPyMethods);
702
703     PYFilterModule = PyImport_ImportModule(_PATH_PYTHON_STARTUP_M);
704     if (PYFilterModule == NULL)
705         syslog(L_ERROR, "failed to import external python module");
706
707     if (PYFilterObject == NULL) {
708         syslog(L_ERROR, "python filter object is not defined");
709         PYfilter(false);
710     } else {
711         PYfilter(true);
712         PYdefmethods();
713         syslog(L_NOTICE, "defined python methods");
714     }
715
716     /* Grab space for these so we aren't forever recreating them.  We also
717        put the body and the line count into PYheaditem, so it needs to be
718        two elements longer than the total number of headers. */
719     PYheaders = PyDict_New();
720     hdrcount = ARRAY_END(ARTheaders) - ARTheaders;
721     PYheaditem = xmalloc((hdrcount + 2) * sizeof(PyObject *));
722     PYheadkey = xmalloc(hdrcount * sizeof(PyObject *));
723
724     /* Preallocate keys for the article dictionary */
725     for (hp = ARTheaders; hp < ARRAY_END(ARTheaders); hp++)
726         PYheadkey[hp - ARTheaders] = PyString_InternFromString(hp->Name);
727     PYpathkey = PyString_InternFromString("Path");
728     PYlineskey = PyString_InternFromString("__LINES__");
729     PYbodykey = PyString_InternFromString("__BODY__");
730 }
731
732 #endif /* defined(DO_PYTHON) */