xref: /openbsd/usr.bin/cu/cu.c (revision f928e069)
1 /* $OpenBSD: cu.c,v 1.31 2024/02/10 15:29:04 deraadt Exp $ */
2 
3 /*
4  * Copyright (c) 2012 Nicholas Marriott <nicm@openbsd.org>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
15  * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
16  * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/types.h>
20 #include <sys/ioctl.h>
21 #include <sys/sysctl.h>
22 
23 #include <ctype.h>
24 #include <err.h>
25 #include <errno.h>
26 #include <event.h>
27 #include <fcntl.h>
28 #include <getopt.h>
29 #include <paths.h>
30 #include <pwd.h>
31 #include <signal.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <termios.h>
36 #include <unistd.h>
37 #include <limits.h>
38 
39 #include "cu.h"
40 
41 extern char		*__progname;
42 
43 FILE			*record_file;
44 struct termios		 saved_tio;
45 struct bufferevent	*input_ev;
46 struct bufferevent	*output_ev;
47 int			 escape_char = '~';
48 int			 is_direct = -1;
49 int			 restricted = 0;
50 const char		*line_path = NULL;
51 int			 line_speed = -1;
52 int			 line_fd;
53 struct termios		 line_tio;
54 struct bufferevent	*line_ev;
55 struct event		 sigterm_ev;
56 struct event		 sighup_ev;
57 enum {
58 	STATE_NONE,
59 	STATE_NEWLINE,
60 	STATE_ESCAPE
61 } last_state = STATE_NEWLINE;
62 
63 __dead void	usage(void);
64 void		signal_event(int, short, void *);
65 void		stream_read(struct bufferevent *, void *);
66 void		stream_error(struct bufferevent *, short, void *);
67 void		line_read(struct bufferevent *, void *);
68 void		line_error(struct bufferevent *, short, void *);
69 void		try_remote(const char *, const char *, const char *);
70 char		*get_ucomnames(void);
71 char 		*find_ucom(const char *, char *);
72 
73 __dead void
usage(void)74 usage(void)
75 {
76 	fprintf(stderr, "usage: %s [-dr] [-E escape_char] [-l line] "
77 	    "[-s speed | -speed]\n", __progname);
78 	fprintf(stderr, "       %s [host]\n", __progname);
79 	exit(1);
80 }
81 
82 int
main(int argc,char ** argv)83 main(int argc, char **argv)
84 {
85 	const char	*errstr;
86 	char		*tmp, *s, *host, *ucomnames;
87 	int		 opt, i, flags;
88 
89 	ucomnames = get_ucomnames();
90 
91 	if (pledge("stdio rpath wpath cpath getpw proc exec tty",
92 	    NULL) == -1)
93 		err(1, "pledge");
94 
95 	if (isatty(STDIN_FILENO) && tcgetattr(STDIN_FILENO, &saved_tio) != 0)
96 		err(1, "tcgetattr");
97 
98 	/*
99 	 * Convert obsolescent -### speed to modern -s### syntax which getopt()
100 	 * can handle.
101 	 */
102 	for (i = 1; i < argc; i++) {
103 		if (strcmp("--", argv[i]) == 0)
104 			break;
105 		if (argv[i][0] != '-' || !isdigit((u_char)argv[i][1]))
106 			continue;
107 
108 		if (asprintf(&argv[i], "-s%s", &argv[i][1]) == -1)
109 			errx(1, "speed asprintf");
110 	}
111 
112 	while ((opt = getopt(argc, argv, "drE:l:s:")) != -1) {
113 		switch (opt) {
114 		case 'd':
115 			is_direct = 1;
116 			break;
117 		case 'r':
118 			if (pledge("stdio rpath wpath tty", NULL) == -1)
119 				err(1, "pledge");
120 			restricted = 1;
121 			break;
122 		case 'E':
123 			if (optarg[0] == '^' && optarg[2] == '\0' &&
124 			    (u_char)optarg[1] >= 64 && (u_char)optarg[1] < 128)
125 				escape_char = (u_char)optarg[1] & 31;
126 			else if (strlen(optarg) == 1)
127 				escape_char = (u_char)optarg[0];
128 			else
129 				errx(1, "invalid escape character: %s", optarg);
130 			break;
131 		case 'l':
132 			line_path = optarg;
133 			break;
134 		case 's':
135 			line_speed = strtonum(optarg, 0, INT_MAX, &errstr);
136 			if (errstr != NULL)
137 				errx(1, "speed is %s: %s", errstr, optarg);
138 			break;
139 		default:
140 			usage();
141 		}
142 	}
143 	argc -= optind;
144 	argv += optind;
145 	if (argc != 0 && argc != 1)
146 		usage();
147 
148 	if (line_path != NULL || line_speed != -1 || is_direct != -1) {
149 		if (argc != 0)
150 			usage();
151 	} else {
152 		if (argc == 1)
153 			host = argv[0];
154 		else
155 			host = getenv("HOST");
156 		if (host != NULL && *host != '\0') {
157 			if (*host == '/')
158 				line_path = host;
159 			else {
160 				s = getenv("REMOTE");
161 				if (s != NULL && *s == '/')
162 					try_remote(host, s, NULL);
163 				else
164 					try_remote(host, NULL, s);
165 			}
166 		}
167 	}
168 
169 	if (line_path == NULL)
170 		line_path = "/dev/cua00";
171 	if (line_speed == -1)
172 		line_speed = 9600;
173 	if (is_direct == -1)
174 		is_direct = 0;
175 
176 	if (strncasecmp(line_path, "usb", 3) == 0) {
177 		tmp = find_ucom(line_path, ucomnames);
178 		if (tmp == NULL)
179 			errx(1, "No ucom matched '%s'", line_path);
180 		line_path = tmp;
181 	}
182 	if (strchr(line_path, '/') == NULL) {
183 		if (asprintf(&tmp, "%s%s", _PATH_DEV, line_path) == -1)
184 			err(1, "asprintf");
185 		line_path = tmp;
186 	}
187 
188 	flags = O_RDWR;
189 	if (is_direct)
190 		flags |= O_NONBLOCK;
191 	line_fd = open(line_path, flags);
192 	if (line_fd == -1)
193 		err(1, "open(\"%s\")", line_path);
194 	if (restricted && pledge("stdio tty", NULL) == -1)
195 		err(1, "pledge");
196 	if (!isatty(line_fd))
197 		err(1, "%s", line_path);
198 	if (ioctl(line_fd, TIOCEXCL) != 0)
199 		err(1, "ioctl(TIOCEXCL)");
200 	if (tcgetattr(line_fd, &line_tio) != 0)
201 		err(1, "tcgetattr");
202 	if (set_line(line_speed) != 0)
203 		err(1, "tcsetattr");
204 
205 	event_init();
206 
207 	signal_set(&sigterm_ev, SIGTERM, signal_event, NULL);
208 	signal_add(&sigterm_ev, NULL);
209 	signal_set(&sighup_ev, SIGHUP, signal_event, NULL);
210 	signal_add(&sighup_ev, NULL);
211 	if (signal(SIGINT, SIG_IGN) == SIG_ERR)
212 		err(1, "signal");
213 	if (signal(SIGQUIT, SIG_IGN) == SIG_ERR)
214 		err(1, "signal");
215 
216 	set_termios(); /* after this use cu_err and friends */
217 
218 	/* stdin and stdout get separate events */
219 	input_ev = bufferevent_new(STDIN_FILENO, stream_read, NULL,
220 	    stream_error, NULL);
221 	bufferevent_enable(input_ev, EV_READ);
222 	output_ev = bufferevent_new(STDOUT_FILENO, NULL, NULL, stream_error,
223 	    NULL);
224 	bufferevent_enable(output_ev, EV_WRITE);
225 
226 	set_blocking(line_fd, 0);
227 	line_ev = bufferevent_new(line_fd, line_read, NULL, line_error,
228 	    NULL);
229 	bufferevent_enable(line_ev, EV_READ|EV_WRITE);
230 
231 	printf("Connected to %s (speed %d)\r\n", line_path, line_speed);
232 	event_dispatch();
233 
234 	restore_termios();
235 	printf("\r\n[EOT]\n");
236 
237 	exit(0);
238 }
239 
240 void
signal_event(int fd,short events,void * data)241 signal_event(int fd, short events, void *data)
242 {
243 	restore_termios();
244 	printf("\r\n[SIG%s]\n", sys_signame[fd]);
245 
246 	exit(0);
247 }
248 
249 void
set_blocking(int fd,int state)250 set_blocking(int fd, int state)
251 {
252 	int mode;
253 
254 	state = state ? 0 : O_NONBLOCK;
255 	if ((mode = fcntl(fd, F_GETFL)) == -1)
256 		cu_err(1, "fcntl");
257 	if ((mode & O_NONBLOCK) != state) {
258 		mode = (mode & ~O_NONBLOCK) | state;
259 		if (fcntl(fd, F_SETFL, mode) == -1)
260 			cu_err(1, "fcntl");
261 	}
262 }
263 
264 void
set_termios(void)265 set_termios(void)
266 {
267 	struct termios tio;
268 
269 	if (!isatty(STDIN_FILENO))
270 		return;
271 
272 	memcpy(&tio, &saved_tio, sizeof(tio));
273 	tio.c_lflag &= ~(ICANON|IEXTEN|ECHO);
274 	tio.c_iflag &= ~(INPCK|ICRNL);
275 	tio.c_oflag &= ~OPOST;
276 	tio.c_cc[VMIN] = 1;
277 	tio.c_cc[VTIME] = 0;
278 	tio.c_cc[VDISCARD] = _POSIX_VDISABLE;
279 	tio.c_cc[VDSUSP] = _POSIX_VDISABLE;
280 	tio.c_cc[VINTR] = _POSIX_VDISABLE;
281 	tio.c_cc[VLNEXT] = _POSIX_VDISABLE;
282 	tio.c_cc[VQUIT] = _POSIX_VDISABLE;
283 	tio.c_cc[VSUSP] = _POSIX_VDISABLE;
284 	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &tio) != 0)
285 		cu_err(1, "tcsetattr");
286 }
287 
288 void
restore_termios(void)289 restore_termios(void)
290 {
291 	if (isatty(STDIN_FILENO))
292 		tcsetattr(STDIN_FILENO, TCSAFLUSH, &saved_tio);
293 }
294 
295 int
set_line(int speed)296 set_line(int speed)
297 {
298 	struct termios	 tio;
299 
300 	memcpy(&tio, &line_tio, sizeof(tio));
301 	tio.c_iflag &= ~(ISTRIP|ICRNL);
302 	tio.c_oflag &= ~OPOST;
303 	tio.c_lflag &= ~(ICANON|ISIG|IEXTEN|ECHO);
304 	tio.c_cflag &= ~(CSIZE|PARENB);
305 	tio.c_cflag |= CREAD|CS8|CLOCAL;
306 	tio.c_cc[VMIN] = 1;
307 	tio.c_cc[VTIME] = 0;
308 	cfsetspeed(&tio, speed);
309 	if (tcsetattr(line_fd, TCSAFLUSH, &tio) != 0)
310 		return (-1);
311 	return (0);
312 }
313 
314 void
stream_read(struct bufferevent * bufev,void * data)315 stream_read(struct bufferevent *bufev, void *data)
316 {
317 	char	*new_data, *ptr;
318 	size_t	 new_size;
319 	int	 state_change;
320 
321 	new_data = EVBUFFER_DATA(input_ev->input);
322 	new_size = EVBUFFER_LENGTH(input_ev->input);
323 	if (new_size == 0)
324 		return;
325 
326 	state_change = isatty(STDIN_FILENO);
327 	for (ptr = new_data; ptr < new_data + new_size; ptr++) {
328 		switch (last_state) {
329 		case STATE_NONE:
330 			if (state_change && *ptr == '\r')
331 				last_state = STATE_NEWLINE;
332 			break;
333 		case STATE_NEWLINE:
334 			if (state_change && (u_char)*ptr == escape_char) {
335 				last_state = STATE_ESCAPE;
336 				continue;
337 			}
338 			if (*ptr != '\r')
339 				last_state = STATE_NONE;
340 			break;
341 		case STATE_ESCAPE:
342 			do_command(*ptr);
343 			last_state = STATE_NEWLINE;
344 			continue;
345 		}
346 
347 		bufferevent_write(line_ev, ptr, 1);
348 	}
349 
350 	evbuffer_drain(input_ev->input, new_size);
351 }
352 
353 void
stream_error(struct bufferevent * bufev,short what,void * data)354 stream_error(struct bufferevent *bufev, short what, void *data)
355 {
356 	event_loopexit(NULL);
357 }
358 
359 void
line_read(struct bufferevent * bufev,void * data)360 line_read(struct bufferevent *bufev, void *data)
361 {
362 	char	*new_data;
363 	size_t	 new_size;
364 
365 	new_data = EVBUFFER_DATA(line_ev->input);
366 	new_size = EVBUFFER_LENGTH(line_ev->input);
367 	if (new_size == 0)
368 		return;
369 
370 	if (record_file != NULL)
371 		fwrite(new_data, 1, new_size, record_file);
372 	bufferevent_write(output_ev, new_data, new_size);
373 
374 	evbuffer_drain(line_ev->input, new_size);
375 }
376 
377 void
line_error(struct bufferevent * bufev,short what,void * data)378 line_error(struct bufferevent *bufev, short what, void *data)
379 {
380 	event_loopexit(NULL);
381 }
382 
383 void
try_remote(const char * host,const char * path,const char * entry)384 try_remote(const char *host, const char *path, const char *entry)
385 {
386 	const char	*paths[] = { "/etc/remote", NULL, NULL };
387 	char		*cp, *s;
388 	long		 l;
389 	int		 error;
390 
391 	if (path != NULL) {
392 		paths[0] = path;
393 		paths[1] = "/etc/remote";
394 	}
395 
396 	if (entry != NULL && cgetset(entry) != 0)
397 		cu_errx(1, "cgetset failed");
398 	error = cgetent(&cp, (char **)paths, (char *)host);
399 	if (error < 0) {
400 		switch (error) {
401 		case -1:
402 			cu_errx(1, "unknown host %s", host);
403 		case -2:
404 			cu_errx(1, "can't open remote file");
405 		case -3:
406 			cu_errx(1, "loop in remote file");
407 		default:
408 			cu_errx(1, "unknown error in remote file");
409 		}
410 	}
411 
412 	if (is_direct == -1 && cgetcap(cp, "dc", ':') != NULL)
413 		is_direct = 1;
414 
415 	if (line_path == NULL && cgetstr(cp, "dv", &s) >= 0)
416 		line_path = s;
417 
418 	if (line_speed == -1 && cgetnum(cp, "br", &l) >= 0) {
419 		if (l < 0 || l > INT_MAX)
420 			cu_errx(1, "speed out of range");
421 		line_speed = l;
422 	}
423 }
424 
425 /* Expands tildes in the file name. Based on code from ssh/misc.c. */
426 char *
tilde_expand(const char * filename1)427 tilde_expand(const char *filename1)
428 {
429 	const char	*filename, *path, *sep;
430 	char		 user[128], *out;
431 	struct passwd	*pw;
432 	u_int		 len, slash;
433 	int		 rv;
434 
435 	if (*filename1 != '~')
436 		goto no_change;
437 	filename = filename1 + 1;
438 
439 	path = strchr(filename, '/');
440 	if (path != NULL && path > filename) {		/* ~user/path */
441 		slash = path - filename;
442 		if (slash > sizeof(user) - 1)
443 			goto no_change;
444 		memcpy(user, filename, slash);
445 		user[slash] = '\0';
446 		if ((pw = getpwnam(user)) == NULL)
447 			goto no_change;
448 	} else if ((pw = getpwuid(getuid())) == NULL)	/* ~/path */
449 		goto no_change;
450 
451 	/* Make sure directory has a trailing '/' */
452 	len = strlen(pw->pw_dir);
453 	if (len == 0 || pw->pw_dir[len - 1] != '/')
454 		sep = "/";
455 	else
456 		sep = "";
457 
458 	/* Skip leading '/' from specified path */
459 	if (path != NULL)
460 		filename = path + 1;
461 
462 	if ((rv = asprintf(&out, "%s%s%s", pw->pw_dir, sep, filename)) == -1)
463 		cu_err(1, "asprintf");
464 	if (rv >= PATH_MAX) {
465 		free(out);
466 		goto no_change;
467 	}
468 
469 	return (out);
470 
471 no_change:
472 	out = strdup(filename1);
473 	if (out == NULL)
474 		cu_err(1, "strdup");
475 	return (out);
476 }
477 
478 char *
get_ucomnames(void)479 get_ucomnames(void)
480 {
481 	char *names;
482 	int mib[2];
483 	size_t size;
484 
485 	mib[0] = CTL_HW;
486 	mib[1] = HW_UCOMNAMES;
487 	names = NULL;
488 	size = 0;
489 	for (;;) {
490 		if (sysctl(mib, 2, NULL, &size, NULL, 0) == -1 || size == 0)
491 			return NULL;
492 		if ((names = realloc(names, size)) == NULL)
493 			err(1, NULL);
494 		if (sysctl(mib, 2, names, &size, NULL, 0) != -1)
495 			break;
496 		if (errno != ENOMEM)
497 			return NULL;
498 	}
499 	return names;
500 }
501 
502 char *
find_ucom(const char * usbid,char * names)503 find_ucom(const char *usbid, char *names)
504 {
505 	char *cua, *id, *ucom;
506 
507 	if (names == NULL)
508 		return NULL;
509 
510 	/* names is a comma separated list of "ucom<unit#>:<usb id>". */
511 	cua = NULL;
512 	for (ucom = strsep(&names, ","); ucom; ucom = strsep(&names, ",")) {
513 		if (*ucom == '\0' || strncasecmp(ucom, "ucom", 4))
514 			continue;
515 		ucom += 4;
516 		id = strchr(ucom, ':');
517 		if (id == NULL)
518 			continue;
519 		*id++ = '\0';
520 		if (strcasecmp(id, usbid) == 0) {
521 			if (asprintf(&cua, "cuaU%s", ucom) == -1)
522 				err(1, NULL);
523 			break;
524 		}
525 	}
526 	return cua;
527 }
528