1 /*
2  * NCMP (netnews control message protocol).
3  * Implement the Usenet control messages, as per RFCs 1036 and 850.
4  * These are fairly infrequent and can afford to be done by
5  * separate programs.  They are:
6  *
7  * control messages that (request a) change (in) the local system:
8  *	cancel message-ID(s)		restricted to Sender: else From: (or
9  *					root?), in theory
10  *	newgroup groupname [moderated]	must be Approved:
11  *	rmgroup groupname		must be Approved:;
12  *					allow some local control
13  *	checkgroups			harass newsadmin about "deviations"
14  *					in active; incompletely specified
15  *
16  * control messages that cause mail back to Reply-To: else From:
17  *	sendsys [site]
18  *	version
19  *
20  * the "ihave/sendme" protocol to minimise traffic volume and maximise delay
21  * between this site and another
22  *	ihave [message-ID-list] remotesys	generate a sendme for remotesys
23  *						from message-ID-list
24  *	sendme [message-ID-list] remotesys	send articles named to remotesys
25  */
26 
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <unistd.h>
30 #include <ctype.h>
31 #include <string.h>
32 #include <errno.h>
33 #include "fixerrno.h"
34 #include <sys/types.h>
35 #include <sys/wait.h>
36 
37 #include "libc.h"
38 #include "news.h"
39 #include "case.h"
40 #include "config.h"
41 #include "headers.h"
42 #include "relay.h"
43 #include "active.h"
44 #include "history.h"
45 #include "rerror.h"
46 
47 #define DEFMSGIDS 10			/* default msgids for awksplit */
48 
49 #define NO_FILES ""
50 #define SUBDIR binfile("ctl")		/* holds shell scripts */
51 
52 /*
53  * These are shell meta-characters, except for /, which is included
54  * since it allows people to escape from the control directory.
55  */
56 #define SHELLMETAS "\\<>|^&;\n({$=*?[`'\"/"
57 
58 /* imports from news */
59 extern statust snufffiles();
60 extern void ihave(), sendme();
61 
62 extern int awksplit(char *string, register char ***fieldsp, register int nfields, const char *sep);
63 
64 /*
65  * In theory (RFC 1036 nee 850), we should verify that the user issuing the
66  * cancel (the Sender: of this article or From: if no Sender) is the
67  * Sender: or From: of the original article or the local super-user.
68  *
69  * In practice, this is a lot of work and since anyone can forge news (and
70  * thus cancel anything), not worth the effort.  Furthermore, there is no
71  * known precise algorithm for matching addresses (given the address
72  * mangling performed by old news systems).
73  *
74  * Ignore ST_ACCESS while cancelling an already-seen article since the
75  * article may have been cancelled before or may have a fake history entry
76  * because the cancel arrived before the article.
77  *
78  * If the article being cancelled has not been seen yet, generate a history
79  * file entry for the cancelled article in case it arrives after the cancel
80  * control.  The history file entry will cause the cancelled article to be
81  * rejected as a duplicate.
82  */
83 STATIC statust
cancelart(msgidstr)84 cancelart(msgidstr)
85 char *msgidstr;
86 {
87 	register char *msgid = strsave(msgidstr);
88 	register statust status = ST_OKAY;
89 
90 	if (msgid[0] == '\0')
91 		;
92 	else if (alreadyseen(msgid)) {
93 		register char *histent, *filelist;
94 
95 		histent = gethistory(msgid);
96 		if (histent != NULL && (filelist = findfiles(histent)) != NULL)
97 			status |= snufffiles(filelist) & ~ST_ACCESS;
98 	} else {
99 		status |= fakehist(msgid, DEFEXP, NO_FILES);	/* start log */
100 		(void) putchar('\n');			/* end log line */
101 	}
102 	free(msgid);
103 	return status;
104 }
105 
106 STATIC statust
cancel(msgids)107 cancel(msgids)
108 char *msgids;
109 {
110 	register int msgidcnt, i;
111 	register statust status = ST_OKAY;
112 	char *msgid[DEFMSGIDS];
113 	char **msgidp = msgid;
114 	char *msgidcpy = strsave(msgids);
115 
116 	msgidcnt = awksplit(msgidcpy, &msgidp, DEFMSGIDS, " \t\n");
117 	if (msgidp == NULL) {
118 		persistent(NOART, 'm', "awksplit failed to allocate memory",
119 			"");
120 		status |= ST_DROPPED|ST_NEEDATTN;
121 	} else {
122 		for (i = 0; i < msgidcnt; i++)
123 			status |= cancelart(msgidp[i]);
124 		if (msgidp != msgid)
125 			free((char *)msgidp);
126 	}
127 	free(msgidcpy);
128 	return status;
129 }
130 
131 /*
132  * log the failure of cmd with status cmdstat, and _exit with bad status
133  * (again avoid stdio buffer flushing in the child).
134  */
135 /* ARGSUSED cmdstat */
136 STATIC void
bombctlmsg(cmd,cmdstat)137 bombctlmsg(cmd, cmdstat)
138 char *cmd;
139 int cmdstat;
140 {
141 	register char *mailcmd;
142 
143 	mailcmd = str3save("PATH=", newspath(), " ; report 'ctl msg failure'");
144 	if (mailcmd == NULL) {
145 		persistent(NOART, 'm', "can't allocate memory in bombctlmsg",
146 			   "");
147 		(void) fflush(stderr);
148 		_exit(1);
149 	}
150 
151 	logaudit(NOART, 'c', "control message `%s' failed", cmd);
152 #ifdef notdef
153 	{
154 		/* TODO: don't do this */
155 		register FILE *mailf = popen(mailcmd, "w");
156 
157 		if (mailf == NULL)
158 			mailf = stderr;
159 		(void) fprintf(mailf,
160 			       "%s: control message `%s' exited with status 0%o\n",
161 			       progname, cmd, cmdstat);
162 		(void) fflush(mailf);
163 		if (mailf != stderr)
164 			(void) pclose(mailf);
165 	}
166 #endif					/* notdef */
167 
168 	free(mailcmd);
169 	_exit(1);
170 }
171 
172 boolean
safecmd(cmd)173 safecmd(cmd)			/* true if it's safe to system(3) cmd */
174 register char *cmd;
175 {
176 	register const char *s;
177 
178 	for (s = cmd; *s != '\0'; s++)
179 		if (STREQN(s, "..", STRLEN("..")))
180 			return NO;
181 	for (s = SHELLMETAS; *s != '\0'; s++)
182 		if (strchr(cmd, *s) != NULL)
183 			return NO;
184 	return YES;
185 }
186 
187 /*
188  * Execute a non-builtin control message by searching $NEWSCTL/bin and
189  * $NEWSBIN/ctl for the command named by the control message.
190  * runctlmsg is called from a child of relaynews, so it must always
191  * call _exit() rather than exit() to avoid flushing stdio buffers.
192  *
193  * Enforce at least minimal security: the environment was standardised at
194  * startup, including PATH and IFS; close non-standard file descriptors;
195  * reject shell metacharacters in ctlcmd.
196  */
197 STATIC void
runctlmsg(ctlcmd,inname)198 runctlmsg(ctlcmd, inname)			/* child process */
199 register char *ctlcmd, *inname;
200 {
201 	register char *cmd, *s1, *s2, *s3;
202 	register int cmdstat;
203 
204 	nolock();
205 	closeall(1);
206 	if (!safecmd(ctlcmd)) {
207 		errno = 0;
208 		logaudit(NOART, 'c', "control `%s' looks unsafe to execute",
209 			 ctlcmd);
210 		(void) fflush(stderr);
211 		_exit(0);		/* it's okay; happens all the time */
212 	}
213 	s1 = str3save("PATH=", ctlfile("bin"), ":");
214 	s2 = str3save(SUBDIR, "; ", "");
215 	s3 = str3save(ctlcmd, " <", inname);
216 	cmd = str3save(s1, s2, s3);
217 	free(s1);
218 	free(s2);
219 	free(s3);
220 	/* TODO: use fork, putenv, exec[vl]p here instead of system? */
221 	cmdstat = system(cmd);
222 	if (cmdstat != 0)
223 		bombctlmsg(cmd, cmdstat);
224 	free(cmd);
225 	_exit(0);
226 }
227 
228 STATIC boolean
ismsgnamed(line,msg)229 ismsgnamed(line, msg)
230 register char *line;
231 register char *msg;
232 {
233 	register int msglen = strlen(msg);
234 
235 	return STREQN(line, msg, msglen) &&
236 	    isascii(line[msglen]) && isspace(line[msglen]);
237 }
238 
239 /*
240  * Implement control message specified in "art".
241  * Because newgroup and rmgroup may modify the active file, for example,
242  * we must flush in-core caches to disk first and reload them afterward.
243  * We handle cancels in this process for speed and dbm read/write access.
244  * We handle ihave & sendme in this process for dbm read access and
245  * to work around syntax restrictions (<>).
246  *
247  * In future, one could pass header values to scripts as arguments or
248  * in environment, as NEWS* variables, to save time in the scripts.
249  */
250 void
ctlmsg(art)251 ctlmsg(art)
252 register struct article *art;
253 {
254 	register char *inname = art->a_tmpf, *ctlcmd = art->h.h_ctlcmd;
255 	int pid, deadpid;
256 	int wstatus;
257 	static char nmcancel[] = "cancel";
258 	static char nmihave[] =  "ihave";
259 	static char nmsendme[] = "sendme";
260 
261 	/* anything to do? */
262 	if (ctlcmd == NULL)
263 		ctlcmd = art->h.h_etctlcmd;
264 	if (ctlcmd == NULL)
265 		return;
266 
267 	/* internal ctl msg (cancel, cancel typo, ihave, sendme)? */
268 	if (ismsgnamed(ctlcmd, nmcancel)) {
269 		art->a_status |= cancel(ctlcmd + STRLEN(nmcancel));
270 		return;
271 	}
272 	if (ctlcmd[0] == '<' || CISTREQN(ctlcmd, nmcancel, STRLEN(nmcancel)) &&
273 	    (!isascii(ctlcmd[STRLEN(nmcancel)]) ||
274 	     !isalpha(ctlcmd[STRLEN(nmcancel)]))) {
275 		/* should really just log this */
276 		errno = 0;
277 		logaudit(NOART, 'c', "malformed cancel `%s'", ctlcmd);
278 		/* no need to return bad status; happens all the time */
279 		return;
280 	}
281 	if (ismsgnamed(ctlcmd, nmihave)) {
282 		ihave(ctlcmd + STRLEN(nmihave), art);
283 		return;
284 	}
285 	if (ismsgnamed(ctlcmd, nmsendme)) {
286 		sendme(ctlcmd + STRLEN(nmsendme), art);
287 		return;
288 	}
289 
290 	/* external ctl msg: flush active and stdio */
291 	art->a_status |= actsync();
292 	(void) fflush(stdout);
293 	(void) fflush(stderr);
294 
295 	pid = fork();
296 	if (pid == 0)				/* child? */
297 		runctlmsg(ctlcmd, inname);
298 	else if (pid == -1)
299 		persistent(art, 'f', "fork failed", "");
300 
301 	/* lint complains about &wstatus on 4.2+BSD; too bad, lint's wrong. */
302 	while ((deadpid = wait(&wstatus)) != pid && deadpid != -1)
303 		;
304 
305 	/* wrong kid returned, fork failed or child screwed up? */
306 	if (deadpid == -1 || pid == -1 || wstatus != 0)
307 		transient(art, '\0', "", "");
308 			/* ctl msg failed; admin got err.msg. by mail */
309 	/* let lazy evaluation load the caches */
310 }
311 
312 char *
hackhybrid(line)313 hackhybrid(line)
314 register char *line;
315 {
316 	static char stupersedes[] = "Supersedes:";
317 	static char alsocan[] =     "Also-Control: cancel ";
318 
319 	return CISTREQN(line, stupersedes, STRLEN(stupersedes))?
320 	    str3save(alsocan, "", &line[STRLEN(stupersedes)]): strsave(line);
321 }
322