--- /dev/null
+/* $Id: misc.c 6535 2003-12-10 09:02:22Z rra $
+**
+** Miscellaneous support routines.
+*/
+
+#include "config.h"
+#include "clibrary.h"
+
+/* Needed on AIX 4.1 to get fd_set and friends. */
+#ifdef HAVE_SYS_SELECT_H
+# include <sys/select.h>
+#endif
+
+#include "inn/innconf.h"
+#include "nnrpd.h"
+#include "tls.h"
+#include "sasl_config.h"
+
+#ifdef HAVE_SSL
+extern SSL *tls_conn;
+extern int nnrpd_starttls_done;
+#endif
+
+
+/*
+** Parse a string into a NULL-terminated array of words; return number
+** of words. If argvp isn't NULL, it and what it points to will be freed.
+*/
+int
+Argify(line, argvp)
+ char *line;
+ char ***argvp;
+{
+ char **argv;
+ char *p;
+
+ if (*argvp != NULL) {
+ free(*argvp[0]);
+ free(*argvp);
+ }
+
+ /* Copy the line, which we will split up. */
+ while (ISWHITE(*line))
+ line++;
+ p = xstrdup(line);
+
+ /* Allocate worst-case amount of space. */
+ for (*argvp = argv = xmalloc((strlen(p) + 2) * sizeof(char *)); *p; ) {
+ /* Mark start of this word, find its end. */
+ for (*argv++ = p; *p && !ISWHITE(*p); )
+ p++;
+ if (*p == '\0')
+ break;
+
+ /* Nip off word, skip whitespace. */
+ for (*p++ = '\0'; ISWHITE(*p); )
+ p++;
+ }
+ *argv = NULL;
+ return argv - *argvp;
+}
+
+
+/*
+** Take a vector which Argify made and glue it back together with
+** spaces between each element. Returns a pointer to dynamic space.
+*/
+char *
+Glom(av)
+ char **av;
+{
+ char **v;
+ int i;
+ char *save;
+
+ /* Get space. */
+ for (i = 0, v = av; *v; v++)
+ i += strlen(*v) + 1;
+ i++;
+
+ save = xmalloc(i);
+ save[0] = '\0';
+ for (v = av; *v; v++) {
+ if (v > av)
+ strlcat(save, " ", i);
+ strlcat(save, *v, i);
+ }
+
+ return save;
+}
+
+
+/*
+** Match a list of newsgroup specifiers against a list of newsgroups.
+** func is called to see if there is a match.
+*/
+bool PERMmatch(char **Pats, char **list)
+{
+ int i;
+ char *p;
+ int match = false;
+
+ if (Pats == NULL || Pats[0] == NULL)
+ return true;
+
+ for ( ; *list; list++) {
+ for (i = 0; (p = Pats[i]) != NULL; i++) {
+ if (p[0] == '!') {
+ if (uwildmat(*list, ++p))
+ match = false;
+ }
+ else if (uwildmat(*list, p))
+ match = true;
+ }
+ if (match)
+ /* If we can read it in one group, we can read it, period. */
+ return true;
+ }
+
+ return false;
+}
+
+
+/*
+** Check to see if user is allowed to see this article by matching
+** Newsgroups line.
+*/
+bool
+PERMartok(void)
+{
+ static char **grplist;
+ char *p, **grp;
+
+ if (!PERMspecified)
+ return false;
+
+ if ((p = GetHeader("Xref")) == NULL) {
+ /* in case article does not include Xref */
+ if ((p = GetHeader("Newsgroups")) != NULL) {
+ if (!NGgetlist(&grplist, p))
+ /* No newgroups or null entry. */
+ return true;
+ } else {
+ return true;
+ }
+ } else {
+ /* skip path element */
+ if ((p = strchr(p, ' ')) == NULL)
+ return true;
+ for (p++ ; *p == ' ' ; p++);
+ if (*p == '\0')
+ return true;
+ if (!NGgetlist(&grplist, p))
+ /* No newgroups or null entry. */
+ return true;
+ /* chop ':' and article number */
+ for (grp = grplist ; *grp != NULL ; grp++) {
+ if ((p = strchr(*grp, ':')) == NULL)
+ return true;
+ *p = '\0';
+ }
+ }
+
+#ifdef DO_PYTHON
+ if (PY_use_dynamic) {
+ char *reply;
+
+ /* Authorize user at a Python authorization module */
+ if (PY_dynamic(PERMuser, p, false, &reply) < 0) {
+ syslog(L_NOTICE, "PY_dynamic(): authorization skipped due to no Python dynamic method defined.");
+ } else {
+ if (reply != NULL) {
+ syslog(L_TRACE, "PY_dynamic() returned a refuse string for user %s at %s who wants to read %s: %s", PERMuser, ClientHost, p, reply);
+ free(reply);
+ return false;
+ }
+ return true;
+ }
+ }
+#endif /* DO_PYTHON */
+
+ return PERMmatch(PERMreadlist, grplist);
+}
+
+
+/*
+** Parse a newsgroups line, return true if there were any.
+*/
+bool
+NGgetlist(argvp, list)
+ char ***argvp;
+ char *list;
+{
+ char *p;
+
+ for (p = list; *p; p++)
+ if (*p == ',')
+ *p = ' ';
+
+ return Argify(list, argvp) != 0;
+}
+
+
+/*********************************************************************
+ * POSTING RATE LIMITS - The following code implements posting rate
+ * limits. News clients are indexed by IP number (or PERMuser, see
+ * config file). After a relatively configurable number of posts, the nnrpd
+ * process will sleep for a period of time before posting anything.
+ *
+ * Each time that IP number posts a message, the time of
+ * posting and the previous sleep time is stored. The new sleep time
+ * is computed based on these values.
+ *
+ * To compute the new sleep time, the previous sleep time is, for most
+ * cases multiplied by a factor (backoff_k).
+ *
+ * See inn.conf(5) for how this code works
+ *
+ *********************************************************************/
+
+/* Defaults are pass through, i.e. not enabled
+ * NEW for INN 1.8 - Use the inn.conf file to specify the following:
+ *
+ * backoff_k: <integer>
+ * backoff_postfast: <integer>
+ * backoff_postslow: <integer>
+ * backoff_trigger: <integer>
+ * backoff_db: <path>
+ * backoff_auth: <on|off>
+ *
+ * You may also specify posting backoffs on a per user basis. To do this
+ * turn on "backoff_auth"
+ *
+ * Now these are runtime constants. <grin>
+ */
+static char postrec_dir[SMBUF]; /* Where is the post record directory? */
+
+void
+InitBackoffConstants()
+{
+ struct stat st;
+
+ /* Default is not to enable this code */
+ BACKOFFenabled = false;
+
+ /* Read the runtime config file to get parameters */
+
+ if ((PERMaccessconf->backoff_db == NULL) ||
+ !(PERMaccessconf->backoff_k >= 0L && PERMaccessconf->backoff_postfast >= 0L && PERMaccessconf->backoff_postslow >= 1L))
+ return;
+
+ /* Need this database for backing off */
+ strlcpy(postrec_dir, PERMaccessconf->backoff_db, sizeof(postrec_dir));
+ if (stat(postrec_dir, &st) < 0) {
+ if (ENOENT == errno) {
+ if (!MakeDirectory(postrec_dir, true)) {
+ syslog(L_ERROR, "%s cannot create backoff_db '%s': %s",ClientHost,postrec_dir,strerror(errno));
+ return;
+ }
+ } else {
+ syslog(L_ERROR, "%s cannot stat backoff_db '%s': %s",ClientHost,postrec_dir,strerror(errno));
+ return;
+ }
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ syslog(L_ERROR, "%s backoff_db '%s' is not a directory",ClientHost,postrec_dir);
+ return;
+ }
+
+ BACKOFFenabled = true;
+
+ return;
+}
+
+/*
+ * PostRecs are stored in individual files. I didn't have a better
+ * way offhand, don't want to touch DBZ, and the number of posters is
+ * small compared to the number of readers. This is the filename corresponding
+ * to an IP number.
+ */
+char
+*PostRecFilename(ip,user)
+ char *ip;
+ char *user;
+{
+ static char buff[SPOOLNAMEBUFF];
+ char dirbuff[SPOOLNAMEBUFF];
+ struct in_addr inaddr;
+ unsigned long int addr;
+ unsigned char quads[4];
+ unsigned int i;
+
+ if (PERMaccessconf->backoff_auth) {
+ snprintf(buff, sizeof(buff), "%s/%s", postrec_dir, user);
+ return(buff);
+ }
+
+ if (inet_aton(ip, &inaddr) < 1) {
+ /* If inet_aton() fails, we'll assume it's an IPv6 address. We'll
+ * also assume for now that we're dealing with a limited number of
+ * IPv6 clients so we'll place their files all in the same
+ * directory for simplicity. Someday we'll need to change this to
+ * something more scalable such as DBZ when IPv6 clients become
+ * more popular. */
+ snprintf(buff, sizeof(buff), "%s/%s", postrec_dir, ip);
+ return(buff);
+ }
+ /* If it's an IPv4 address just fall through. */
+
+ addr = ntohl(inaddr.s_addr);
+ for (i=0; i<4; i++)
+ quads[i] = (unsigned char) (0xff & (addr>>(i*8)));
+
+ snprintf(dirbuff, sizeof(dirbuff), "%s/%03d%03d/%03d",
+ postrec_dir, quads[3], quads[2], quads[1]);
+ if (!MakeDirectory(dirbuff,true)) {
+ syslog(L_ERROR, "%s Unable to create postrec directories '%s': %s",
+ ClientHost, dirbuff, strerror(errno));
+ return NULL;
+ }
+ snprintf(buff, sizeof(buff), "%s/%03d", dirbuff, quads[0]);
+ return(buff);
+}
+
+/*
+ * Lock the post rec file. Return 1 on lock, 0 on error
+ */
+int
+LockPostRec(path)
+ char *path;
+{
+ char lockname[SPOOLNAMEBUFF];
+ char temp[SPOOLNAMEBUFF];
+ int statfailed = 0;
+
+ snprintf(lockname, sizeof(lockname), "%s.lock", path);
+
+ for (;; sleep(5)) {
+ int fd;
+ struct stat st;
+ time_t now;
+
+ fd = open(lockname, O_WRONLY|O_EXCL|O_CREAT, 0600);
+ if (fd >= 0) {
+ /* We got the lock! */
+ snprintf(temp, sizeof(temp), "pid:%ld\n", (unsigned long) getpid());
+ write(fd, temp, strlen(temp));
+ close(fd);
+ return(1);
+ }
+
+ /* No lock. See if the file is there. */
+ if (stat(lockname, &st) < 0) {
+ syslog(L_ERROR, "%s cannot stat lock file %s", ClientHost, strerror(errno));
+ if (statfailed++ > 5) return(0);
+ continue;
+ }
+
+ /* If lockfile is older than the value of
+ PERMaccessconf->backoff_postslow, remove it */
+ statfailed = 0;
+ time(&now);
+ if (now < st.st_ctime + PERMaccessconf->backoff_postslow) continue;
+ syslog(L_ERROR, "%s removing stale lock file %s", ClientHost, lockname);
+ unlink(lockname);
+ }
+}
+
+void
+UnlockPostRec(path)
+ char *path;
+{
+ char lockname[SPOOLNAMEBUFF];
+
+ snprintf(lockname, sizeof(lockname), "%s.lock", path);
+ if (unlink(lockname) < 0) {
+ syslog(L_ERROR, "%s can't unlink lock file: %s", ClientHost,strerror(errno)) ;
+ }
+ return;
+}
+
+/*
+ * Get the stored postrecord for that IP
+ */
+static int
+GetPostRecord(char *path, long *lastpost, long *lastsleep, long *lastn)
+{
+ static char buff[SMBUF];
+ FILE *fp;
+ char *s;
+
+ fp = fopen(path,"r");
+ if (fp == NULL) {
+ if (errno == ENOENT) {
+ return 1;
+ }
+ syslog(L_ERROR, "%s Error opening '%s': %s",
+ ClientHost, path, strerror(errno));
+ return 0;
+ }
+
+ if (fgets(buff,SMBUF,fp) == NULL) {
+ syslog(L_ERROR, "%s Error reading '%s': %s",
+ ClientHost, path, strerror(errno));
+ return 0;
+ }
+ *lastpost = atol(buff);
+
+ if ((s = strchr(buff,',')) == NULL) {
+ syslog(L_ERROR, "%s bad data in postrec file: '%s'",
+ ClientHost, buff);
+ return 0;
+ }
+ s++; *lastsleep = atol(s);
+
+ if ((s = strchr(s,',')) == NULL) {
+ syslog(L_ERROR, "%s bad data in postrec file: '%s'",
+ ClientHost, buff);
+ return 0;
+ }
+ s++; *lastn = atol(s);
+
+ fclose(fp);
+ return 1;
+}
+
+/*
+ * Store the postrecord for that IP
+ */
+static int
+StorePostRecord(char *path, time_t lastpost, long lastsleep, long lastn)
+{
+ FILE *fp;
+
+ fp = fopen(path,"w");
+ if (fp == NULL) {
+ syslog(L_ERROR, "%s Error opening '%s': %s",
+ ClientHost, path, strerror(errno));
+ return 0;
+ }
+
+ fprintf(fp,"%ld,%ld,%ld\n",(long) lastpost,lastsleep,lastn);
+ fclose(fp);
+ return 1;
+}
+
+/*
+ * Return the proper sleeptime. Return false on error.
+ */
+int
+RateLimit(sleeptime,path)
+ long *sleeptime;
+ char *path;
+{
+ TIMEINFO Now;
+ long prevpost,prevsleep,prevn,n;
+
+ if (GetTimeInfo(&Now) < 0)
+ return 0;
+
+ prevpost = 0L; prevsleep = 0L; prevn = 0L; n = 0L;
+ if (!GetPostRecord(path,&prevpost,&prevsleep,&prevn)) {
+ syslog(L_ERROR, "%s can't get post record: %s",
+ ClientHost, strerror(errno));
+ return 0;
+ }
+ /*
+ * Just because yer paranoid doesn't mean they ain't out ta get ya
+ * This is called paranoid clipping
+ */
+ if (prevn < 0L) prevn = 0L;
+ if (prevsleep < 0L) prevsleep = 0L;
+ if (prevsleep > PERMaccessconf->backoff_postfast) prevsleep = PERMaccessconf->backoff_postfast;
+
+ /*
+ * Compute the new sleep time
+ */
+ *sleeptime = 0L;
+ if (prevpost <= 0L) {
+ prevpost = 0L;
+ prevn = 1L;
+ } else {
+ n = Now.time - prevpost;
+ if (n < 0L) {
+ syslog(L_NOTICE,"%s previous post was in the future (%ld sec)",
+ ClientHost,n);
+ n = 0L;
+ }
+ if (n < PERMaccessconf->backoff_postfast) {
+ if (prevn >= PERMaccessconf->backoff_trigger) {
+ *sleeptime = 1 + (prevsleep * PERMaccessconf->backoff_k);
+ }
+ } else if (n < PERMaccessconf->backoff_postslow) {
+ if (prevn >= PERMaccessconf->backoff_trigger) {
+ *sleeptime = prevsleep;
+ }
+ } else {
+ prevn = 0L;
+ }
+ prevn++;
+ }
+
+ *sleeptime = ((*sleeptime) > PERMaccessconf->backoff_postfast) ? PERMaccessconf->backoff_postfast : (*sleeptime);
+ /* This ought to trap this bogon */
+ if ((*sleeptime) < 0L) {
+ syslog(L_ERROR,"%s Negative sleeptime detected: %ld, prevsleep: %ld, N: %ld",ClientHost,*sleeptime,prevsleep,n);
+ *sleeptime = 0L;
+ }
+
+ /* Store the postrecord */
+ if (!StorePostRecord(path,Now.time,*sleeptime,prevn)) {
+ syslog(L_ERROR, "%s can't store post record: %s", ClientHost, strerror(errno));
+ return 0;
+ }
+
+ return 1;
+}
+
+#ifdef HAVE_SSL
+/*
+** The "STARTTLS" command. RFC2595.
+*/
+/* ARGSUSED0 */
+
+void
+CMDstarttls(ac, av)
+ int ac UNUSED;
+ char *av[] UNUSED;
+{
+ int result;
+
+ tls_init();
+ if (nnrpd_starttls_done == 1) {
+ Reply("%d Already successfully executed STARTTLS\r\n",
+ NNTP_STARTTLS_DONE_VAL);
+ return;
+ }
+
+ Reply("%d Begin TLS negotiation now\r\n", NNTP_STARTTLS_NEXT_VAL);
+ fflush(stdout);
+
+ /* must flush our buffers before starting tls */
+
+ result=tls_start_servertls(0, /* read */
+ 1); /* write */
+ if (result==-1) {
+ Reply("%d Starttls failed\r\n", NNTP_STARTTLS_BAD_VAL);
+ return;
+ }
+ nnrpd_starttls_done = 1;
+}
+#endif /* HAVE_SSL */