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