+/* $Id: perl.c 7815 2008-05-05 08:43:58Z iulius $
+**
+** Perl filtering support for innd.
+**
+** Originally written by Christophe Wolfhugel <wolf@pasteur.fr> (although
+** he wouldn't recognise it anymore so don't blame him) and modified,
+** expanded and tweaked since by James Brister, Jeremy Nixon, Ed Mooring,
+** Russell Vincent, and Russ Allbery.
+**
+** This file should contain all innd-specific Perl linkage. Linkage
+** applicable to both innd and nnrpd should go into lib/perl.c instead.
+**
+** We are assuming Perl 5.004 or later.
+**
+** Future work:
+**
+** - What we're doing with Path headers right now doesn't work for folded
+** headers. It's also kind of gross. There has to be a better way of
+** handling this.
+**
+** - The breakdown between this file, lib/perl.c, and nnrpd/perl.c should
+** be rethought, ideally in the light of supporting multiple filters in
+** different languages.
+**
+** - We're still calling strlen() on artBody, which should be avoidable
+** since we've already walked it several times. We should just cache
+** the length somewhere for speed.
+**
+** - Variable and key names should be standardized between this and nnrpd.
+**
+** - The XS code is still calling CC* functions. The common code between
+** the two control interfaces should be factored out into the rest of
+** innd instead.
+**
+** - There's a needless perl_get_cv() call for *every message ID* offered
+** to the server right now. We need to stash whether that filter is
+** active.
+*/
+
+#include "config.h"
+
+/* Skip this entire file if DO_PERL (./configure --with-perl) isn't set. */
+#if DO_PERL
+
+#include "clibrary.h"
+#include "innd.h"
+
+#include <EXTERN.h>
+#include <perl.h>
+#include <XSUB.h>
+#include "ppport.h"
+
+#include "innperl.h"
+
+/* From art.c. Ew. Need header parsing that doesn't use globals. */
+extern char *filterPath;
+
+/*
+** Run an incoming article through the Perl article filter. Returns NULL
+** accept the article or a rejection message to reject it.
+*/
+char *
+PLartfilter(const ARTDATA *data, char *artBody, long artLen, int lines)
+{
+ dSP;
+ const ARTHEADER * hp;
+ const HDRCONTENT *hc = data->HdrContent;
+ HV * hdr;
+ CV * filter;
+ int i, rc;
+ char * p;
+ static SV * body = NULL;
+ static char buf[256];
+
+ if (!PerlFilterActive) return NULL;
+ filter = perl_get_cv("filter_art", 0);
+ if (!filter) return NULL;
+
+ /* Create %hdr and stash a copy of every known header. Path has to be
+ handled separately since it's been munged by article processing. */
+ hdr = perl_get_hv("hdr", 1);
+ for (i = 0 ; i < MAX_ARTHEADER ; i++) {
+ if (HDR_FOUND(i)) {
+ hp = &ARTheaders[i];
+ hv_store(hdr, (char *) hp->Name, hp->Size, newSVpv(HDR(i), 0), 0);
+ }
+ }
+
+ /* Store the article body. We don't want to make another copy of it,
+ since it could potentially be quite large. Instead, stash the
+ pointer in the static SV * body. We set LEN to 0 and inc the
+ refcount to tell Perl not to free it (either one should be enough).
+ Requires 5.004. In testing, this produced a 17% speed improvement
+ over making a copy of the article body for a fairly heavy filter. */
+ if (artBody) {
+ if (!body) {
+ body = newSV(0);
+ (void) SvUPGRADE(body, SVt_PV);
+ }
+ SvPVX(body) = artBody;
+ SvCUR_set(body, artLen);
+ SvLEN_set(body, 0);
+ SvPOK_on(body);
+ (void) SvREADONLY_on(body);
+ (void) SvREFCNT_inc(body);
+ hv_store(hdr, "__BODY__", 8, body, 0);
+ }
+
+ hv_store(hdr, "__LINES__", 9, newSViv(lines), 0);
+
+ ENTER;
+ SAVETMPS;
+ PUSHMARK(SP);
+ rc = perl_call_sv((SV *) filter, G_EVAL|G_SCALAR|G_NOARGS);
+ SPAGAIN;
+
+ hv_undef(hdr);
+
+ /* Check $@, which will be set if the sub died. */
+ buf[0] = '\0';
+ if (SvTRUE(ERRSV)) {
+ syslog(L_ERROR, "Perl function filter_art died on article %s: %s",
+ HDR_FOUND(HDR__MESSAGE_ID) ? HDR(HDR__MESSAGE_ID) : "?",
+ SvPV(ERRSV, PL_na));
+ (void) POPs;
+ PerlFilter(false);
+ } else if (rc == 1) {
+ p = POPp;
+ if (p && *p)
+ strlcpy(buf, p, sizeof(buf));
+ }
+
+ PUTBACK;
+ FREETMPS;
+ LEAVE;
+ return (buf[0] != '\0') ? buf : NULL;
+}
+
+
+/*
+** Run an incoming message ID from CHECK or IHAVE through the Perl filter.
+** Returns NULL to accept the article or a rejection message to reject it.
+*/
+char *
+PLmidfilter(char *messageID)
+{
+ dSP;
+ CV *filter;
+ int rc;
+ char *p;
+ static char buf[256];
+
+ if (!PerlFilterActive) return NULL;
+ filter = perl_get_cv("filter_messageid", 0);
+ if (!filter) return NULL;
+
+ /* Pass filter_messageid() the message ID on the Perl stack. */
+ ENTER;
+ SAVETMPS;
+ PUSHMARK(SP);
+ XPUSHs(sv_2mortal(newSVpv(messageID, 0)));
+ PUTBACK;
+ rc = perl_call_sv((SV *) filter, G_EVAL|G_SCALAR);
+ SPAGAIN;
+
+ /* Check $@, which will be set if the sub died. */
+ buf[0] = '\0';
+ if (SvTRUE(ERRSV)) {
+ syslog(L_ERROR, "Perl function filter_messageid died on id %s: %s",
+ messageID, SvPV(ERRSV, PL_na));
+ (void) POPs;
+ PerlFilter(false);
+ } else if (rc == 1) {
+ p = POPp;
+ if (p && *p)
+ strlcpy(buf, p, sizeof(buf));
+ }
+
+ PUTBACK;
+ FREETMPS;
+ LEAVE;
+ return (buf[0] != '\0') ? buf : NULL;
+}
+
+
+/*
+** Call a Perl sub on any change in INN's mode, passing in the old and new
+** mode and the reason.
+*/
+void
+PLmode(OPERATINGMODE Mode, OPERATINGMODE NewMode, char *reason)
+{
+ dSP;
+ HV *mode;
+ CV *filter;
+
+ filter = perl_get_cv("filter_mode", 0);
+ if (!filter) return;
+
+ /* Current mode goes into $mode{Mode}, new mode in $mode{NewMode}, and
+ the reason in $mode{reason}. */
+ mode = perl_get_hv("mode", 1);
+
+ if (Mode == OMrunning)
+ hv_store(mode, "Mode", 4, newSVpv("running", 0), 0);
+ if (Mode == OMpaused)
+ hv_store(mode, "Mode", 4, newSVpv("paused", 0), 0);
+ if (Mode == OMthrottled)
+ hv_store(mode, "Mode", 4, newSVpv("throttled", 0), 0);
+
+ if (NewMode == OMrunning)
+ hv_store(mode, "NewMode", 7, newSVpv("running", 0), 0);
+ if (NewMode == OMpaused)
+ hv_store(mode, "NewMode", 7, newSVpv("paused", 0), 0);
+ if (NewMode == OMthrottled)
+ hv_store(mode, "NewMode", 7, newSVpv("throttled", 0), 0);
+
+ hv_store(mode, "reason", 6, newSVpv(reason, 0), 0);
+
+ PUSHMARK(SP);
+ perl_call_sv((SV *) filter, G_EVAL|G_DISCARD|G_NOARGS);
+
+ /* Check $@, which will be set if the sub died. */
+ if (SvTRUE(ERRSV)) {
+ syslog(L_ERROR, "Perl function filter_mode died: %s",
+ SvPV(ERRSV, PL_na));
+ (void) POPs;
+ PerlFilter(false);
+ }
+}
+
+
+/*
+** Called by CCmode, this returns the Perl filter statistics if a Perl
+** function to generate such statistics has been defined, or NULL otherwise.
+** If a string is returned, it's in newly allocated memory that must be freed
+** by the caller.
+*/
+char *
+PLstats(void)
+{
+ dSP;
+ char *argv[] = { NULL };
+
+ if (perl_get_cv("filter_stats", false) == NULL)
+ return NULL;
+ else {
+ char *stats = NULL;
+ char *result;
+
+ ENTER;
+ SAVETMPS;
+ perl_call_argv("filter_stats", G_EVAL | G_NOARGS, argv);
+ SPAGAIN;
+ result = POPp;
+ if (result != NULL && *result)
+ stats = xstrdup(result);
+ PUTBACK;
+ FREETMPS;
+ LEAVE;
+
+ return stats;
+ }
+}
+
+
+/*
+** The remainder of this file are XS callbacks visible to embedded Perl
+** code to perform various innd functions. They were originally written by
+** Ed Mooring (mooring@acm.org) on May 14, 1998, and have since been split
+** between this file and lib/perl.c (which has the ones that can also be
+** used in nnrpd). The function that registers them at startup is at the
+** end.
+*/
+
+/*
+** Add an entry to history. Takes message ID and optionally arrival,
+** article, and expire times and storage API token. If the times aren't
+** given, they default to now. If the token isn't given, that field will
+** be left empty. Returns boolean success.
+*/
+XS(XS_INN_addhist)
+{
+ dXSARGS;
+ int i;
+ char tbuff[32];
+ char* parambuf[6];
+
+ if (items < 1 || items > 5)
+ croak("Usage INN::addhist(msgid,[arrival,articletime,expire,token])");
+
+ for (i = 0; i < items; i++)
+ parambuf[i] = (char *) SvPV(ST(0), PL_na);
+
+ /* If any of the times are missing, they should default to now. */
+ if (i < 4) {
+ snprintf(tbuff, sizeof(tbuff), "%ld", (long) time(NULL));
+ for (; i < 4; i++)
+ parambuf[i] = tbuff;
+ }
+
+ /* The token defaults to an empty string. */
+ if (i == 4)
+ parambuf[4] = "";
+
+ parambuf[5] = NULL;
+
+ /* CCaddhist returns NULL on success. */
+ if (CCaddhist(parambuf))
+ XSRETURN_NO;
+ else
+ XSRETURN_YES;
+}
+
+
+/*
+** Takes the message ID of an article and returns the full article as a
+** string or undef if the article wasn't found. It will be converted from
+** wire format to native format. Note that this call isn't particularly
+** optimized or cheap.
+*/
+XS(XS_INN_article)
+{
+ dXSARGS;
+ char * msgid;
+ TOKEN token;
+ ARTHANDLE * art;
+ char * p;
+ size_t len;
+
+ if (items != 1)
+ croak("Usage: INN::article(msgid)");
+
+ /* Get the article token from the message ID and the history file. */
+ msgid = (char *) SvPV(ST(0), PL_na);
+ if (!HISlookup(History, msgid, NULL, NULL, NULL, &token)) XSRETURN_UNDEF;
+
+ /* Retrieve the article and convert it from wire format. */
+ art = SMretrieve(token, RETR_ALL);
+ if (art == NULL) XSRETURN_UNDEF;
+ p = FromWireFmt(art->data, art->len, &len);
+ SMfreearticle(art);
+
+ /* Push a copy of the article onto the Perl stack, free our temporary
+ memory allocation, and return the article to Perl. */
+ ST(0) = sv_2mortal(newSVpv(p, len));
+ free(p);
+ XSRETURN(1);
+}
+
+
+/*
+** Cancel a message by message ID; returns boolean success. Equivalent to
+** ctlinnd cancel <message>.
+*/
+XS(XS_INN_cancel)
+{
+ dXSARGS;
+ char *msgid;
+ char *parambuf[2];
+
+ if (items != 1)
+ croak("Usage: INN::cancel(msgid)");
+
+ msgid = (char *) SvPV(ST(0), PL_na);
+ parambuf[0] = msgid;
+ parambuf[1] = NULL;
+
+ /* CCcancel returns NULL on success. */
+ if (CCcancel(parambuf))
+ XSRETURN_NO;
+ else
+ XSRETURN_YES;
+}
+
+
+/*
+** Return the files for a given message ID, taken from the history file.
+** This function should really be named INN::token() and probably will be
+** some day.
+*/
+XS(XS_INN_filesfor)
+{
+ dXSARGS;
+ char *msgid;
+ TOKEN token;
+
+ if (items != 1)
+ croak("Usage: INN::filesfor(msgid)");
+
+ msgid = (char *) SvPV(ST(0), PL_na);
+ if (HISlookup(History, msgid, NULL, NULL, NULL, &token)) {
+ XSRETURN_PV(TokenToText(token));
+ } else {
+ XSRETURN_UNDEF;
+ }
+}
+
+
+/*
+** Whether message ID is in the history file; returns boolean.
+*/
+XS(XS_INN_havehist)
+{
+ dXSARGS;
+ char *msgid;
+
+ if (items != 1)
+ croak("Usage: INN::havehist(msgid)");
+
+ msgid = (char *) SvPV(ST(0), PL_na);
+ if (HIScheck(History, msgid))
+ XSRETURN_YES;
+ else
+ XSRETURN_NO;
+}
+
+
+/*
+** Takes the message ID of an article and returns the article headers as
+** a string or undef if the article wasn't found. Each line of the header
+** will end with \n.
+*/
+XS(XS_INN_head)
+{
+ dXSARGS;
+ char * msgid;
+ TOKEN token;
+ ARTHANDLE * art;
+ char * p;
+ size_t len;
+
+ if (items != 1)
+ croak("Usage: INN::head(msgid)");
+
+ /* Get the article token from the message ID and the history file. */
+ msgid = (char *) SvPV(ST(0), PL_na);
+ if (!HISlookup(History, msgid, NULL, NULL, NULL, &token)) XSRETURN_UNDEF;
+
+ /* Retrieve the article header and convert it from wire format. */
+ art = SMretrieve(token, RETR_HEAD);
+ if (art == NULL) XSRETURN_UNDEF;
+ p = FromWireFmt(art->data, art->len, &len);
+ SMfreearticle(art);
+
+ /* Push a copy of the article header onto the Perl stack, free our
+ temporary memory allocation, and return the header to Perl. */
+ ST(0) = sv_2mortal(newSVpv(p, len));
+ free(p);
+ XSRETURN(1);
+}
+
+
+/*
+** Returns the active file flag for a newsgroup or undef if it isn't in the
+** active file.
+*/
+XS(XS_INN_newsgroup)
+{
+ dXSARGS;
+ char * newsgroup;
+ NEWSGROUP * ngp;
+ char * end;
+ int size;
+
+ if (items != 1)
+ croak("Usage: INN::newsgroup(group)");
+ newsgroup = (char *) SvPV(ST(0), PL_na);
+
+ ngp = NGfind(newsgroup);
+ if (!ngp) {
+ XSRETURN_UNDEF;
+ } else {
+ /* ngp->Rest is newline-terminated; find the end. */
+ end = strchr(ngp->Rest, '\n');
+ if (end == NULL) {
+ size = strlen(ngp->Rest);
+ } else {
+ size = end - ngp->Rest;
+ }
+ ST(0) = sv_2mortal(newSVpv(ngp->Rest, size));
+ XSRETURN(1);
+ }
+}
+
+
+/*
+** Initialize the XS callbacks defined in this file.
+*/
+void
+PLxsinit(void)
+{
+ newXS("INN::addhist", XS_INN_addhist, "perl.c");
+ newXS("INN::article", XS_INN_article, "perl.c");
+ newXS("INN::cancel", XS_INN_cancel, "perl.c");
+ newXS("INN::havehist", XS_INN_havehist, "perl.c");
+ newXS("INN::head", XS_INN_head, "perl.c");
+ newXS("INN::newsgroup", XS_INN_newsgroup, "perl.c");
+ newXS("INN::filesfor", XS_INN_filesfor, "perl.c");
+}
+
+#endif /* defined(DO_PERL) */