summaryrefslogtreecommitdiffstats
path: root/src/util/vstream_popen.c
blob: d00d49e66bf4709eeb498b7488e072c058e5d82c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*++
/* NAME
/*	vstream_popen 3
/* SUMMARY
/*	open stream to child process
/* SYNOPSIS
/*	#include <vstream.h>
/*
/*	VSTREAM	*vstream_popen(flags, key, value, ...)
/*	int	flags;
/*	int	key;
/*
/*	int	vstream_pclose(stream)
/*	VSTREAM	*stream;
/* DESCRIPTION
/*	vstream_popen() opens a one-way or two-way stream to a user-specified
/*	command, which is executed by a child process. The \fIflags\fR
/*	argument is as with vstream_fopen(). The child's standard input and
/*	standard output are redirected to the stream, which is based on a
/*	socketpair or other suitable local IPC. vstream_popen() takes a list
/*	of macros with zero or more arguments, terminated by
/*	CA_VSTREAM_POPEN_END.  The following is a listing of macros
/*	with the expected argument type.
/* .RS
/* .IP "CA_VSTREAM_POPEN_COMMAND(const char *)"
/*	Specifies the command to execute as a string. The string is
/*	passed to the shell when it contains shell meta characters
/*	or when it appears to be a shell built-in command, otherwise
/*	the command is executed without invoking a shell.
/*	One of CA_VSTREAM_POPEN_COMMAND or VSTREAM_POPEN_ARGV must be specified.
/* .IP "CA_VSTREAM_POPEN_ARGV(char **)"
/*	The command is specified as an argument vector. This vector is
/*	passed without further inspection to the \fIexecvp\fR() routine.
/*	One of CA_VSTREAM_POPEN_COMMAND or VSTREAM_POPEN_ARGV must be specified.
/*	See also the CA_VSTREAM_POPEN_SHELL attribute below.
/* .IP "CA_VSTREAM_POPEN_ENV(char **)"
/*	Additional environment information, in the form of a null-terminated
/*	list of name, value, name, value, ... elements. By default only the
/*	command search path is initialized to _PATH_DEFPATH.
/* .IP "CA_VSTREAM_POPEN_EXPORT(char **)"
/*	This argument is passed to clean_env().
/*	Null-terminated array of names of environment parameters
/*	that can be exported. By default, everything is exported.
/* .IP "CA_VSTREAM_POPEN_UID(uid_t)"
/*	The user ID to execute the command as. The user ID must be non-zero.
/* .IP "CA_VSTREAM_POPEN_GID(gid_t)"
/*	The group ID to execute the command as. The group ID must be non-zero.
/* .IP "CA_VSTREAM_POPEN_SHELL(const char *)"
/*	The shell to use when executing the command specified with
/*	CA_VSTREAM_POPEN_COMMAND. This shell is invoked regardless of the
/*	command content.
/* .IP "CA_VSTREAM_POPEN_WAITPID_FN(pid_t (*)(pid_t, WAIT_STATUS_T *, int))"
/*	waitpid()-like function to reap the child exit status when
/*	vstream_pclose() is called.
/* .RE
/* .PP
/*	vstream_pclose() closes the named stream and returns the child
/*	exit status. It is an error to specify a stream that was not
/*	returned by vstream_popen() or that is no longer open.
/* DIAGNOSTICS
/*	Panics: interface violations. Fatal errors: out of memory.
/*
/*	vstream_popen() returns a null pointer in case of trouble.
/*	The nature of the problem is specified via the \fIerrno\fR
/*	global variable.
/*
/*	vstream_pclose() returns -1 in case of trouble.
/*	The nature of the problem is specified via the \fIerrno\fR
/*	global variable.
/* SEE ALSO
/*	vstream(3) light-weight buffered I/O
/* BUGS
/*	The interface, stolen from popen()/pclose(), ignores errors
/*	returned when the stream is closed, and does not distinguish
/*	between exit status codes and kill signals.
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*--*/

/* System library. */

#include <sys_defs.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#ifdef USE_PATHS_H
#include <paths.h>
#endif
#include <syslog.h>

/* Utility library. */

#include <msg.h>
#include <exec_command.h>
#include <vstream.h>
#include <argv.h>
#include <set_ugid.h>
#include <clean_env.h>
#include <iostuff.h>

/* Application-specific. */

typedef struct VSTREAM_POPEN_ARGS {
    char  **argv;
    char   *command;
    uid_t   uid;
    gid_t   gid;
    int     privileged;
    char  **env;
    char  **export;
    char   *shell;
    VSTREAM_WAITPID_FN waitpid_fn;
} VSTREAM_POPEN_ARGS;

/* vstream_parse_args - get arguments from variadic list */

static void vstream_parse_args(VSTREAM_POPEN_ARGS *args, va_list ap)
{
    const char *myname = "vstream_parse_args";
    int     key;

    /*
     * First, set the default values (on all non-zero entries)
     */
    args->argv = 0;
    args->command = 0;
    args->uid = 0;
    args->gid = 0;
    args->privileged = 0;
    args->env = 0;
    args->export = 0;
    args->shell = 0;
    args->waitpid_fn = 0;

    /*
     * Then, override the defaults with user-supplied inputs.
     */
    while ((key = va_arg(ap, int)) != VSTREAM_POPEN_END) {
	switch (key) {
	case VSTREAM_POPEN_ARGV:
	    if (args->command != 0)
		msg_panic("%s: got VSTREAM_POPEN_ARGV and VSTREAM_POPEN_COMMAND", myname);
	    args->argv = va_arg(ap, char **);
	    break;
	case VSTREAM_POPEN_COMMAND:
	    if (args->argv != 0)
		msg_panic("%s: got VSTREAM_POPEN_ARGV and VSTREAM_POPEN_COMMAND", myname);
	    args->command = va_arg(ap, char *);
	    break;
	case VSTREAM_POPEN_UID:
	    args->privileged = 1;
	    args->uid = va_arg(ap, uid_t);
	    break;
	case VSTREAM_POPEN_GID:
	    args->privileged = 1;
	    args->gid = va_arg(ap, gid_t);
	    break;
	case VSTREAM_POPEN_ENV:
	    args->env = va_arg(ap, char **);
	    break;
	case VSTREAM_POPEN_EXPORT:
	    args->export = va_arg(ap, char **);
	    break;
	case VSTREAM_POPEN_SHELL:
	    args->shell = va_arg(ap, char *);
	    break;
	case VSTREAM_POPEN_WAITPID_FN:
	    args->waitpid_fn = va_arg(ap, VSTREAM_WAITPID_FN);
	    break;
	default:
	    msg_panic("%s: unknown key: %d", myname, key);
	}
    }

    if (args->command == 0 && args->argv == 0)
	msg_panic("%s: missing VSTREAM_POPEN_ARGV or VSTREAM_POPEN_COMMAND", myname);
    if (args->privileged != 0 && args->uid == 0)
	msg_panic("%s: privileged uid", myname);
    if (args->privileged != 0 && args->gid == 0)
	msg_panic("%s: privileged gid", myname);
}

/* vstream_popen - open stream to child process */

VSTREAM *vstream_popen(int flags,...)
{
    const char *myname = "vstream_popen";
    VSTREAM_POPEN_ARGS args;
    va_list ap;
    VSTREAM *stream;
    int     sockfd[2];
    int     pid;
    int     fd;
    ARGV   *argv;
    char  **cpp;

    va_start(ap, flags);
    vstream_parse_args(&args, ap);
    va_end(ap);

    if (args.command == 0)
	args.command = args.argv[0];

    if (duplex_pipe(sockfd) < 0)
	return (0);

    switch (pid = fork()) {
    case -1:					/* error */
	(void) close(sockfd[0]);
	(void) close(sockfd[1]);
	return (0);
    case 0:					/* child */
	(void) msg_cleanup((MSG_CLEANUP_FN) 0);
	if (close(sockfd[1]))
	    msg_warn("close: %m");
	for (fd = 0; fd < 2; fd++)
	    if (sockfd[0] != fd)
		if (DUP2(sockfd[0], fd) < 0)
		    msg_fatal("dup2: %m");
	if (sockfd[0] >= 2 && close(sockfd[0]))
	    msg_warn("close: %m");

	/*
	 * Don't try to become someone else unless the user specified it.
	 */
	if (args.privileged)
	    set_ugid(args.uid, args.gid);

	/*
	 * Environment plumbing. Always reset the command search path. XXX
	 * That should probably be done by clean_env().
	 */
	if (args.export)
	    clean_env(args.export);
	if (setenv("PATH", _PATH_DEFPATH, 1))
	    msg_fatal("%s: setenv: %m", myname);
	if (args.env)
	    for (cpp = args.env; *cpp; cpp += 2)
		if (setenv(cpp[0], cpp[1], 1))
		    msg_fatal("setenv: %m");

	/*
	 * Process plumbing. If possible, avoid running a shell.
	 */
	closelog();
	if (args.argv) {
	    execvp(args.argv[0], args.argv);
	    msg_fatal("%s: execvp %s: %m", myname, args.argv[0]);
	} else if (args.shell && *args.shell) {
	    argv = argv_split(args.shell, CHARS_SPACE);
	    argv_add(argv, args.command, (char *) 0);
	    argv_terminate(argv);
	    execvp(argv->argv[0], argv->argv);
	    msg_fatal("%s: execvp %s: %m", myname, argv->argv[0]);
	} else {
	    exec_command(args.command);
	}
	/* NOTREACHED */
    default:					/* parent */
	if (close(sockfd[0]))
	    msg_warn("close: %m");
	stream = vstream_fdopen(sockfd[1], flags);
	stream->waitpid_fn = args.waitpid_fn;
	stream->pid = pid;
	return (stream);
    }
}

/* vstream_pclose - close stream to child process */

int     vstream_pclose(VSTREAM *stream)
{
    pid_t   saved_pid = stream->pid;
    VSTREAM_WAITPID_FN saved_waitpid_fn = stream->waitpid_fn;
    pid_t   pid;
    WAIT_STATUS_T wait_status;

    /*
     * Close the pipe. Don't trigger an alarm in vstream_fclose().
     */
    if (saved_pid == 0)
	msg_panic("vstream_pclose: stream has no process");
    stream->pid = 0;
    vstream_fclose(stream);

    /*
     * Reap the child exit status.
     */
    do {
	if (saved_waitpid_fn != 0)
	    pid = saved_waitpid_fn(saved_pid, &wait_status, 0);
	else
	    pid = waitpid(saved_pid, &wait_status, 0);
    } while (pid == -1 && errno == EINTR);
    return (pid == -1 ? -1 :
	    WIFSIGNALED(wait_status) ? WTERMSIG(wait_status) :
	    WEXITSTATUS(wait_status));
}

#ifdef TEST

#include <fcntl.h>
#include <vstring.h>
#include <vstring_vstream.h>

 /*
  * Test program. Run a command and copy lines one by one.
  */
int     main(int argc, char **argv)
{
    VSTRING *buf = vstring_alloc(100);
    VSTREAM *stream;
    int     status;

    /*
     * Sanity check.
     */
    if (argc < 2)
	msg_fatal("usage: %s 'command'", argv[0]);

    /*
     * Open stream to child process.
     */
    if ((stream = vstream_popen(O_RDWR,
				VSTREAM_POPEN_ARGV, argv + 1,
				VSTREAM_POPEN_END)) == 0)
	msg_fatal("vstream_popen: %m");

    /*
     * Copy loop, one line at a time.
     */
    while (vstring_fgets(buf, stream) != 0) {
	if (vstream_fwrite(VSTREAM_OUT, vstring_str(buf), VSTRING_LEN(buf))
	    != VSTRING_LEN(buf))
	    msg_fatal("vstream_fwrite: %m");
	if (vstream_fflush(VSTREAM_OUT) != 0)
	    msg_fatal("vstream_fflush: %m");
	if (vstring_fgets(buf, VSTREAM_IN) == 0)
	    break;
	if (vstream_fwrite(stream, vstring_str(buf), VSTRING_LEN(buf))
	    != VSTRING_LEN(buf))
	    msg_fatal("vstream_fwrite: %m");
    }

    /*
     * Cleanup.
     */
    vstring_free(buf);
    if ((status = vstream_pclose(stream)) != 0)
	msg_warn("exit status: %d", status);

    exit(status);
}

#endif