2 * Copyright (c) 2013-2018 Marc André Tanner <mat at brain-dump.org>
4 * Permission to use, copy, modify, and/or distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
33 #include <sys/select.h>
35 #include <sys/ioctl.h>
36 #include <sys/types.h>
38 #include <sys/socket.h>
40 #if defined(__linux__) || defined(__CYGWIN__)
42 #elif defined(__FreeBSD__) || defined(__DragonFly__)
44 #elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__)
48 #if defined CTRL && defined _AIX
52 #define CTRL(k) ((k) & 0x1F)
58 # include "forkpty-aix.c"
60 # include "forkpty-sunos.c"
63 #define countof(arr) (sizeof(arr) / sizeof((arr)[0]))
78 char msg
[4096 - 2*sizeof(uint32_t)];
88 typedef struct Client Client
;
99 CLIENT_READONLY
= 1 << 0,
100 CLIENT_LOWPRIORITY
= 1 << 1,
112 struct winsize winsize
;
114 volatile sig_atomic_t running
;
116 const char *session_name
;
121 static Server server
= { .running
= true, .exit_status
= -1, .host
= "@localhost" };
122 static Client client
;
123 static struct termios orig_term
, cur_term
;
124 static bool has_term
, alternate_buffer
, quiet
, passthrough
;
126 static struct sockaddr_un sockaddr
= {
127 .sun_family
= AF_UNIX
,
130 static bool set_socket_name(struct sockaddr_un
*sockaddr
, const char *name
);
131 static void die(const char *s
);
132 static void info(const char *str
, ...);
136 static inline size_t packet_header_size() {
137 return offsetof(Packet
, u
);
140 static size_t packet_size(Packet
*pkt
) {
141 return packet_header_size() + pkt
->len
;
144 static ssize_t
write_all(int fd
, const char *buf
, size_t len
) {
145 debug("write_all(%d)\n", len
);
148 ssize_t res
= write(fd
, buf
, len
);
150 if (errno
== EAGAIN
|| errno
== EWOULDBLOCK
|| errno
== EINTR
)
162 static ssize_t
read_all(int fd
, char *buf
, size_t len
) {
163 debug("read_all(%d)\n", len
);
166 ssize_t res
= read(fd
, buf
, len
);
168 if (errno
== EWOULDBLOCK
)
170 if (errno
== EAGAIN
|| errno
== EINTR
)
182 static bool send_packet(int socket
, Packet
*pkt
) {
183 size_t size
= packet_size(pkt
);
184 if (size
> sizeof(*pkt
))
186 return write_all(socket
, (char *)pkt
, size
) == size
;
189 static bool recv_packet(int socket
, Packet
*pkt
) {
190 ssize_t len
= read_all(socket
, (char*)pkt
, packet_header_size());
191 if (len
<= 0 || len
!= packet_header_size())
193 if (pkt
->len
> sizeof(pkt
->u
.msg
)) {
198 len
= read_all(socket
, pkt
->u
.msg
, pkt
->len
);
199 if (len
<= 0 || len
!= pkt
->len
)
208 static void info(const char *str
, ...) {
212 fprintf(stderr
, "%s: %s: ", server
.name
, server
.session_name
);
213 vfprintf(stderr
, str
, ap
);
214 fprintf(stderr
, "\r\n");
220 static void die(const char *s
) {
225 static void usage(void) {
226 fprintf(stderr
, "usage: abduco [-a|-A|-c|-n] [-p] [-r] [-q] [-l] [-f] [-e detachkey] name command\n");
230 static bool xsnprintf(char *buf
, size_t size
, const char *fmt
, ...) {
235 int n
= vsnprintf(buf
, size
, fmt
, ap
);
240 errno
= ENAMETOOLONG
;
246 static int session_connect(const char *name
) {
249 if (!set_socket_name(&sockaddr
, name
) || (fd
= socket(AF_UNIX
, SOCK_STREAM
, 0)) == -1)
251 socklen_t socklen
= offsetof(struct sockaddr_un
, sun_path
) + strlen(sockaddr
.sun_path
) + 1;
252 if (connect(fd
, (struct sockaddr
*)&sockaddr
, socklen
) == -1) {
253 if (errno
== ECONNREFUSED
&& stat(sockaddr
.sun_path
, &sb
) == 0 && S_ISSOCK(sb
.st_mode
))
254 unlink(sockaddr
.sun_path
);
261 static pid_t
session_exists(const char *name
) {
264 if ((server
.socket
= session_connect(name
)) == -1)
266 if (client_recv_packet(&pkt
) && pkt
.type
== MSG_PID
)
268 close(server
.socket
);
272 static bool session_alive(const char *name
) {
274 return session_exists(name
) &&
275 stat(sockaddr
.sun_path
, &sb
) == 0 &&
276 S_ISSOCK(sb
.st_mode
) && (sb
.st_mode
& S_IXGRP
) == 0;
279 static bool create_socket_dir(struct sockaddr_un
*sockaddr
) {
280 sockaddr
->sun_path
[0] = '\0';
281 int socketfd
= socket(AF_UNIX
, SOCK_STREAM
, 0);
285 const size_t maxlen
= sizeof(sockaddr
->sun_path
);
286 uid_t uid
= getuid();
287 struct passwd
*pw
= getpwuid(uid
);
289 for (unsigned int i
= 0; i
< countof(socket_dirs
); i
++) {
291 struct Dir
*dir
= &socket_dirs
[i
];
294 dir
->path
= getenv(dir
->env
);
295 ishome
= !strcmp(dir
->env
, "HOME");
296 if (ishome
&& (!dir
->path
|| !dir
->path
[0]) && pw
)
297 dir
->path
= pw
->pw_dir
;
299 if (!dir
->path
|| !dir
->path
[0])
301 if (!xsnprintf(sockaddr
->sun_path
, maxlen
, "%s/%s%s/", dir
->path
, ishome
? "." : "", server
.name
))
303 mode_t mask
= umask(0);
304 int r
= mkdir(sockaddr
->sun_path
, dir
->personal
? S_IRWXU
: S_IRWXU
|S_IRWXG
|S_IRWXO
|S_ISVTX
);
306 if (r
!= 0 && errno
!= EEXIST
)
308 if (lstat(sockaddr
->sun_path
, &sb
) != 0)
310 if (!S_ISDIR(sb
.st_mode
)) {
315 size_t dirlen
= strlen(sockaddr
->sun_path
);
316 if (!dir
->personal
) {
317 /* create subdirectory only accessible to user */
318 if (pw
&& !xsnprintf(sockaddr
->sun_path
+dirlen
, maxlen
-dirlen
, "%s/", pw
->pw_name
))
320 if (!pw
&& !xsnprintf(sockaddr
->sun_path
+dirlen
, maxlen
-dirlen
, "%d/", uid
))
322 if (mkdir(sockaddr
->sun_path
, S_IRWXU
) != 0 && errno
!= EEXIST
)
324 if (lstat(sockaddr
->sun_path
, &sb
) != 0)
326 if (!S_ISDIR(sb
.st_mode
)) {
330 dirlen
= strlen(sockaddr
->sun_path
);
333 if (sb
.st_uid
!= uid
|| sb
.st_mode
& (S_IRWXG
|S_IRWXO
)) {
338 if (!xsnprintf(sockaddr
->sun_path
+dirlen
, maxlen
-dirlen
, ".abduco-%d", getpid()))
341 socklen_t socklen
= offsetof(struct sockaddr_un
, sun_path
) + strlen(sockaddr
->sun_path
) + 1;
342 if (bind(socketfd
, (struct sockaddr
*)sockaddr
, socklen
) == -1)
344 unlink(sockaddr
->sun_path
);
346 sockaddr
->sun_path
[dirlen
] = '\0';
354 static bool set_socket_name(struct sockaddr_un
*sockaddr
, const char *name
) {
355 const size_t maxlen
= sizeof(sockaddr
->sun_path
);
356 const char *session_name
= NULL
;
359 if (name
[0] == '/') {
360 if (strlen(name
) >= maxlen
) {
361 errno
= ENAMETOOLONG
;
364 strncpy(sockaddr
->sun_path
, name
, maxlen
);
365 } else if (name
[0] == '.' && (name
[1] == '.' || name
[1] == '/')) {
366 char *cwd
= getcwd(buf
, sizeof buf
);
369 if (!xsnprintf(sockaddr
->sun_path
, maxlen
, "%s/%s", cwd
, name
))
372 if (!create_socket_dir(sockaddr
))
374 if (strlen(sockaddr
->sun_path
) + strlen(name
) + strlen(server
.host
) >= maxlen
) {
375 errno
= ENAMETOOLONG
;
379 strncat(sockaddr
->sun_path
, name
, maxlen
- strlen(sockaddr
->sun_path
) - 1);
380 strncat(sockaddr
->sun_path
, server
.host
, maxlen
- strlen(sockaddr
->sun_path
) - 1);
384 strncpy(buf
, sockaddr
->sun_path
, sizeof buf
);
385 session_name
= basename(buf
);
387 setenv("ABDUCO_SESSION", session_name
, 1);
388 setenv("ABDUCO_SOCKET", sockaddr
->sun_path
, 1);
393 static bool create_session(const char *name
, char * const argv
[]) {
394 /* this uses the well known double fork strategy as described in section 1.7 of
396 * http://www.faqs.org/faqs/unix-faq/programmer/faq/
398 * pipes are used for synchronization and error reporting i.e. the child sets
399 * the close on exec flag before calling execvp(3) the parent blocks on a read(2)
400 * in case of failure the error message is written to the pipe, success is
401 * indicated by EOF on the pipe.
403 int client_pipe
[2], server_pipe
[2];
408 if (session_exists(name
)) {
413 if (pipe(client_pipe
) == -1)
415 if ((server
.socket
= server_create_socket(name
)) == -1)
418 switch ((pid
= fork())) {
419 case 0: /* child process */
421 close(client_pipe
[0]);
422 switch ((pid
= fork())) {
423 case 0: /* child process */
424 if (pipe(server_pipe
) == -1) {
425 snprintf(errormsg
, sizeof(errormsg
), "server-pipe: %s\n", strerror(errno
));
426 write_all(client_pipe
[1], errormsg
, strlen(errormsg
));
427 close(client_pipe
[1]);
431 sigemptyset(&sa
.sa_mask
);
432 sa
.sa_handler
= server_pty_died_handler
;
433 sigaction(SIGCHLD
, &sa
, NULL
);
434 switch (server
.pid
= forkpty(&server
.pty
, NULL
, has_term
? &server
.term
: NULL
, &server
.winsize
)) {
435 case 0: /* child = user application process */
436 close(server
.socket
);
437 close(server_pipe
[0]);
438 if (fcntl(client_pipe
[1], F_SETFD
, FD_CLOEXEC
) == 0 &&
439 fcntl(server_pipe
[1], F_SETFD
, FD_CLOEXEC
) == 0)
440 execvp(argv
[0], argv
);
441 snprintf(errormsg
, sizeof(errormsg
), "server-execvp: %s: %s\n",
442 argv
[0], strerror(errno
));
443 write_all(client_pipe
[1], errormsg
, strlen(errormsg
));
444 write_all(server_pipe
[1], errormsg
, strlen(errormsg
));
445 close(client_pipe
[1]);
446 close(server_pipe
[1]);
449 case -1: /* forkpty failed */
450 snprintf(errormsg
, sizeof(errormsg
), "server-forkpty: %s\n", strerror(errno
));
451 write_all(client_pipe
[1], errormsg
, strlen(errormsg
));
452 close(client_pipe
[1]);
453 close(server_pipe
[0]);
454 close(server_pipe
[1]);
457 default: /* parent = server process */
458 sa
.sa_handler
= server_sigterm_handler
;
459 sigaction(SIGTERM
, &sa
, NULL
);
460 sigaction(SIGINT
, &sa
, NULL
);
461 sa
.sa_handler
= server_sigusr1_handler
;
462 sigaction(SIGUSR1
, &sa
, NULL
);
463 sa
.sa_handler
= SIG_IGN
;
464 sigaction(SIGPIPE
, &sa
, NULL
);
465 sigaction(SIGHUP
, &sa
, NULL
);
466 if (chdir("/") == -1)
469 int fd
= open("/dev/null", O_RDWR
);
471 dup2(fd
, STDIN_FILENO
);
472 dup2(fd
, STDOUT_FILENO
);
473 dup2(fd
, STDERR_FILENO
);
477 close(client_pipe
[1]);
478 close(server_pipe
[1]);
479 if (read_all(server_pipe
[0], errormsg
, sizeof(errormsg
)) > 0)
481 close(server_pipe
[0]);
486 case -1: /* fork failed */
487 snprintf(errormsg
, sizeof(errormsg
), "server-fork: %s\n", strerror(errno
));
488 write_all(client_pipe
[1], errormsg
, strlen(errormsg
));
489 close(client_pipe
[1]);
492 default: /* parent = intermediate process */
493 close(client_pipe
[1]);
498 case -1: /* fork failed */
499 close(client_pipe
[0]);
500 close(client_pipe
[1]);
502 default: /* parent = client process */
503 close(client_pipe
[1]);
505 wait(&status
); /* wait for first fork */
506 ssize_t len
= read_all(client_pipe
[0], errormsg
, sizeof(errormsg
));
508 write_all(STDERR_FILENO
, errormsg
, len
);
509 unlink(sockaddr
.sun_path
);
512 close(client_pipe
[0]);
517 static bool attach_session(const char *name
, const bool terminate
) {
518 if (server
.socket
> 0)
519 close(server
.socket
);
520 if ((server
.socket
= session_connect(name
)) == -1)
522 if (server_set_socket_non_blocking(server
.socket
) == -1)
527 sigemptyset(&sa
.sa_mask
);
528 sa
.sa_handler
= client_sigwinch_handler
;
529 sigaction(SIGWINCH
, &sa
, NULL
);
530 sa
.sa_handler
= SIG_IGN
;
531 sigaction(SIGPIPE
, &sa
, NULL
);
533 client_setup_terminal();
534 int status
= client_mainloop();
535 client_restore_terminal();
538 } else if (status
== -EIO
) {
539 info("exited due to I/O errors");
541 info("session terminated with exit status %d", status
);
549 static int session_filter(const struct dirent
*d
) {
550 return strstr(d
->d_name
, server
.host
) != NULL
;
553 static int session_comparator(const struct dirent
**a
, const struct dirent
**b
) {
555 if (stat((*a
)->d_name
, &sa
) != 0)
557 if (stat((*b
)->d_name
, &sb
) != 0)
559 return sa
.st_atime
< sb
.st_atime
? -1 : 1;
562 static int list_session(void) {
563 if (!create_socket_dir(&sockaddr
))
565 if (chdir(sockaddr
.sun_path
) == -1)
567 struct dirent
**namelist
;
568 int n
= scandir(sockaddr
.sun_path
, &namelist
, session_filter
, session_comparator
);
571 printf("Active sessions (on host %s)\n", server
.host
+1);
573 struct stat sb
; char buf
[255];
574 if (stat(namelist
[n
]->d_name
, &sb
) == 0 && S_ISSOCK(sb
.st_mode
)) {
576 strftime(buf
, sizeof(buf
), "%a%t %F %T", localtime(&sb
.st_mtime
));
578 char *local
= strstr(namelist
[n
]->d_name
, server
.host
);
580 *local
= '\0'; /* truncate hostname if we are local */
581 if (!(pid
= session_exists(namelist
[n
]->d_name
)))
584 if (sb
.st_mode
& S_IXUSR
)
586 else if (sb
.st_mode
& S_IXGRP
)
588 printf("%c %s\t%jd\t%s\n", status
, buf
, (intmax_t)pid
, namelist
[n
]->d_name
);
596 int main(int argc
, char *argv
[]) {
599 char **cmd
= NULL
, action
= '\0';
601 char *default_cmd
[4] = { "/bin/sh", "-c", getenv("ABDUCO_CMD"), NULL
};
602 if (!default_cmd
[2]) {
603 default_cmd
[0] = ABDUCO_CMD
;
604 default_cmd
[1] = NULL
;
607 server
.name
= basename(argv
[0]);
608 gethostname(server
.host
+1, sizeof(server
.host
) - 1);
610 while ((opt
= getopt(argc
, argv
, "aAclne:fpqrv")) != -1) {
621 if (optarg
[0] == '^' && optarg
[1])
622 optarg
[0] = CTRL(optarg
[1]);
623 KEY_DETACH
= optarg
[0];
635 client
.flags
|= CLIENT_READONLY
;
638 client
.flags
|= CLIENT_LOWPRIORITY
;
641 puts("abduco-"VERSION
" © 2013-2018 Marc André Tanner");
648 /* collect the session name if trailing args */
650 server
.session_name
= argv
[optind
];
652 /* if yet more trailing arguments, they must be the command */
653 if (optind
+ 1 < argc
)
654 cmd
= &argv
[optind
+ 1];
658 if (server
.session_name
&& !isatty(STDIN_FILENO
))
665 client
.flags
|= CLIENT_LOWPRIORITY
;
668 if (!action
&& !server
.session_name
)
669 exit(list_session());
670 if (!action
|| !server
.session_name
)
673 if (!passthrough
&& tcgetattr(STDIN_FILENO
, &orig_term
) != -1) {
674 server
.term
= orig_term
;
678 if (ioctl(STDIN_FILENO
, TIOCGWINSZ
, &server
.winsize
) == -1) {
679 server
.winsize
.ws_col
= 80;
680 server
.winsize
.ws_row
= 25;
683 server
.read_pty
= (action
== 'n');
690 if (session_alive(server
.session_name
)) {
691 info("session exists and has not yet terminated");
694 if (session_exists(server
.session_name
))
695 attach_session(server
.session_name
, false);
697 if (!create_session(server
.session_name
, cmd
))
698 die("create-session");
702 if (!attach_session(server
.session_name
, true))
703 die("attach-session");
706 if (session_alive(server
.session_name
)) {
707 if (!attach_session(server
.session_name
, true))
708 die("attach-session");
709 } else if (!attach_session(server
.session_name
, !force
)) {