/ libpkg / fetch_ssh.c
fetch_ssh.c
  1  /*-
  2   * Copyright (c) 2020 Baptiste Daroussin <bapt@FreeBSD.org>
  3   * Copyright (c) 2023 Serenity Cyber Security, LLC <license@futurecrew.ru>
  4   *                    Author: Gleb Popov <arrowd@FreeBSD.org>
  5   *
  6   * Redistribution and use in source and binary forms, with or without
  7   * modification, are permitted provided that the following conditions
  8   * are met:
  9   * 1. Redistributions of source code must retain the above copyright
 10   *    notice, this list of conditions and the following disclaimer
 11   *    in this position and unchanged.
 12   * 2. Redistributions in binary form must reproduce the above copyright
 13   *    notice, this list of conditions and the following disclaimer in the
 14   *    documentation and/or other materials provided with the distribution.
 15   *
 16   * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
 17   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 18   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 19   * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
 20   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 21   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 22   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 23   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 24   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 25   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 26   */
 27  
 28  #include <sys/param.h>
 29  #include <sys/wait.h>
 30  #include <sys/socket.h>
 31  #include <sys/time.h>
 32  #include <sys/types.h>
 33  
 34  #include <ctype.h>
 35  #include <fcntl.h>
 36  #include <errno.h>
 37  #include <stdio.h>
 38  #include <string.h>
 39  #include <paths.h>
 40  #include <poll.h>
 41  #include <netdb.h>
 42  #include <time.h>
 43  
 44  #include <bsd_compat.h>
 45  
 46  #include "pkg.h"
 47  #include "private/event.h"
 48  #include "private/pkg.h"
 49  #include "private/fetch.h"
 50  #include "private/utils.h"
 51  #include "yuarel.h"
 52  
 53  #ifndef timespeccmp
 54  #define	timespeccmp(tsp, usp, cmp)					\
 55  	(((tsp)->tv_sec == (usp)->tv_sec) ?				\
 56  	    ((tsp)->tv_nsec cmp (usp)->tv_nsec) :			\
 57  	    ((tsp)->tv_sec cmp (usp)->tv_sec))
 58  #endif
 59  #ifndef timespecsub
 60  #define	timespecsub(tsp, usp, vsp)					\
 61  	do {								\
 62  		(vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec;		\
 63  		(vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec;	\
 64  		if ((vsp)->tv_nsec < 0) {				\
 65  			(vsp)->tv_sec--;				\
 66  			(vsp)->tv_nsec += 1000000000L;			\
 67  		}							\
 68  	} while (0)
 69  #endif
 70  
 71  static int ssh_read(void *data, char *buf, int len);
 72  static int ssh_write(void *data, const char *buf, int l);
 73  static int ssh_close(void *data);
 74  static int tcp_close(void *data);
 75  
 76  static int
 77  tcp_connect(struct pkg_repo *repo, struct yuarel *u)
 78  {
 79  	char *line = NULL;
 80  	size_t linecap = 0;
 81  	struct addrinfo *ai = NULL, *curai, hints;
 82  	char srv[NI_MAXSERV];
 83  	int sd = -1;
 84  	int retcode;
 85  
 86  	pkg_dbg(PKG_DBG_FETCH, 1, "TCP> tcp_connect");
 87  	memset(&hints, 0, sizeof(hints));
 88  	hints.ai_family = PF_UNSPEC;
 89  	if (repo->ip == IPV4)
 90  		hints.ai_family = PF_INET;
 91  	else if (repo->ip == IPV6)
 92  		hints.ai_family = PF_INET6;
 93  	hints.ai_socktype = SOCK_STREAM;
 94  	snprintf(srv, sizeof(srv), "%d", u->port);
 95  	retcode = getaddrinfo(u->host, srv, &hints, &ai);
 96  	if (retcode != 0) {
 97  		pkg_emit_pkg_errno(EPKG_NONETWORK, "tcp_connect", gai_strerror(retcode));
 98  		pkg_emit_error("Unable to lookup for '%s'", u->host);
 99  		return (EPKG_FATAL);
100  	}
101  	for (curai = ai; curai != NULL; curai = curai->ai_next) {
102  		if ((sd = socket(curai->ai_family, curai->ai_socktype,
103  		    curai->ai_protocol)) == -1)
104  			continue;
105  		if (connect(sd, curai->ai_addr, curai->ai_addrlen) == -1) {
106  			close(sd);
107  			sd = -1;
108  			continue;
109  		}
110  		break;
111  	}
112  	freeaddrinfo(ai);
113  	if (sd == -1) {
114  		pkg_emit_pkg_errno(EPKG_NONETWORK, "tcp_connect", NULL);
115  		pkg_emit_error("Could not connect to tcp://%s:%d", u->host,
116  		    u->port);
117  		return (EPKG_FATAL);
118  	}
119  	if (setsockopt(sd, SOL_SOCKET, SO_KEEPALIVE, &(int){ 1 }, sizeof(int)) != 0) {
120  		pkg_emit_errno("Could not connect", "setsockopt");
121  		close(sd);
122  		return (EPKG_FATAL);
123  	}
124  	repo->sshio.in = dup(sd);
125  	repo->sshio.out = dup(sd);
126  	repo->fh = funopen(repo, ssh_read, ssh_write, NULL, tcp_close);
127  
128  	retcode = EPKG_FATAL;
129  	if (repo->fh == NULL) {
130  		pkg_emit_errno("Failed to open stream", "tcp_connect");
131  		goto tcp_cleanup;
132  	}
133  
134  	if (getline(&line, &linecap, repo->fh) > 0) {
135  		if (strncmp(line, "ok:", 3) != 0) {
136  			pkg_dbg(PKG_DBG_FETCH, 1, "SSH> server rejected, got: %s", line);
137  			goto tcp_cleanup;
138  		}
139  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> server is: %s", line +4);
140  	} else {
141  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> nothing to read, got: %s", line);
142  		goto tcp_cleanup;
143  	}
144  	retcode = EPKG_OK;
145  tcp_cleanup:
146  	if (retcode == EPKG_FATAL && repo->fh != NULL) {
147  		fclose(repo->fh);
148  		repo->fh = NULL;
149  	}
150  	free(line);
151  	return (retcode);
152  }
153  
154  static int
155  ssh_connect(struct pkg_repo *repo, struct yuarel *u)
156  {
157  	char *line = NULL;
158  	size_t linecap = 0;
159  	int sshin[2];
160  	int sshout[2];
161  	xstring *cmd = NULL;
162  	char *cmdline;
163  	int retcode = EPKG_FATAL;
164  	const char *ssh_args;
165  	const char *argv[4];
166  
167  	/* Use socket pair because pipe have blocking issues */
168  	if (socketpair(AF_UNIX, SOCK_STREAM, 0, sshin) <0 ||
169  			socketpair(AF_UNIX, SOCK_STREAM, 0, sshout) < 0)
170  		return(EPKG_FATAL);
171  
172  	repo->sshio.pid = fork();
173  	if (repo->sshio.pid == -1) {
174  		pkg_emit_errno("Cannot fork", "start_ssh");
175  		goto ssh_cleanup;
176  	}
177  
178  	if (repo->sshio.pid == 0) {
179  
180  		if (dup2(sshin[0], STDIN_FILENO) < 0 ||
181  				close(sshin[1]) < 0 ||
182  				close(sshout[0]) < 0 ||
183  				dup2(sshout[1], STDOUT_FILENO) < 0) {
184  			pkg_emit_errno("Cannot prepare pipes", "start_ssh");
185  			goto ssh_cleanup;
186  		}
187  
188  		cmd = xstring_new();
189  		fputs("/usr/bin/ssh -e none -T ", cmd->fp);
190  
191  		ssh_args = repo->ssh_args;
192  		if (ssh_args == NULL)
193  			ssh_args = pkg_object_string(
194  			    pkg_config_get("PKG_SSH_ARGS"));
195  		if (ssh_args != NULL)
196  			fprintf(cmd->fp, "%s ", ssh_args);
197  		if (repo->ip == IPV4)
198  			fputs("-4 ", cmd->fp);
199  		else if (repo->ip == IPV6)
200  			fputs("-6 ", cmd->fp);
201  		if (u->port > 0)
202  			fprintf(cmd->fp, "-p %d ", u->port);
203  		if (u->username != NULL)
204  			fprintf(cmd->fp, "%s@", u->username);
205  		fprintf(cmd->fp, "%s pkg ssh", u->host);
206  		cmdline = xstring_get(cmd);
207  		pkg_dbg(PKG_DBG_FETCH, 1, "Fetch: running '%s'", cmdline);
208  		argv[0] = _PATH_BSHELL;
209  		argv[1] = "-c";
210  		argv[2] = cmdline;
211  		argv[3] = NULL;
212  
213  		if (sshin[0] != STDIN_FILENO)
214  			close(sshin[0]);
215  		if (sshout[1] != STDOUT_FILENO)
216  			close(sshout[1]);
217  		execvp(argv[0], __DECONST(char **, argv));
218  		/* NOT REACHED */
219  	}
220  
221  	if (close(sshout[1]) < 0 || close(sshin[0]) < 0) {
222  		pkg_emit_errno("Failed to close pipes", "start_ssh");
223  		goto ssh_cleanup;
224  	}
225  
226  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> connected");
227  
228  	repo->sshio.in = sshout[0];
229  	repo->sshio.out = sshin[1];
230  	set_nonblocking(repo->sshio.in);
231  
232  	repo->fh = funopen(repo, ssh_read, ssh_write, NULL, ssh_close);
233  	if (repo->fh == NULL) {
234  		pkg_emit_errno("Failed to open stream", "start_ssh");
235  		goto ssh_cleanup;
236  	}
237  
238  	if (getline(&line, &linecap, repo->fh) > 0) {
239  		if (strncmp(line, "ok:", 3) != 0) {
240  			pkg_dbg(PKG_DBG_FETCH, 1, "SSH> server rejected, got: %s", line);
241  			goto ssh_cleanup;
242  		}
243  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> server is: %s", line +4);
244  	} else {
245  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> nothing to read, got: %s", line);
246  		goto ssh_cleanup;
247  	}
248  	retcode = EPKG_OK;
249  
250  ssh_cleanup:
251  	if (retcode == EPKG_FATAL && repo->fh != NULL) {
252  		fclose(repo->fh);
253  		repo->fh = NULL;
254  	}
255  	free(line);
256  	return (retcode);
257  }
258  
259  static int
260  pkgprotocol_open(struct pkg_repo *repo, struct fetch_item *fi,
261      int (*proto_connect)(struct pkg_repo *, struct yuarel *))
262  {
263  	char *line = NULL;
264  	size_t linecap = 0;
265  	size_t linelen;
266  	const char *errstr;
267  	int retcode = EPKG_FATAL;
268  	struct yuarel url;
269  	char *url_to_free = xstrdup(fi->url);
270  
271  	if (yuarel_parse(&url, url_to_free) == -1) {
272  		free(url_to_free);
273  		pkg_emit_error("Invalid URL: '%s'", fi->url);
274  		return (EPKG_FATAL);
275  	}
276  
277  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> tcp_open");
278  	if (repo->fh == NULL)
279  		retcode = proto_connect(repo, &url);
280  	else
281  		retcode = EPKG_OK;
282  
283  	if (retcode != EPKG_OK)
284  		return (retcode);
285  
286  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> get %s %" PRIdMAX "", url.path, (intmax_t)fi->mtime);
287  	fprintf(repo->fh, "get %s %" PRIdMAX "\n", url.path, (intmax_t)fi->mtime);
288  	if ((linelen = getline(&line, &linecap, repo->fh)) > 0) {
289  		if (line[linelen -1 ] == '\n')
290  			line[linelen -1 ] = '\0';
291  
292  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> recv: %s", line);
293  		if (strncmp(line, "ok:", 3) == 0) {
294  			fi->size = strtonum(line + 4, 0, LONG_MAX, &errstr);
295  			if (errstr) {
296  				goto out;
297  			}
298  
299  			if (fi->size == 0) {
300  				retcode = EPKG_UPTODATE;
301  				goto out;
302  			}
303  
304  			retcode = EPKG_OK;
305  			goto out;
306  		}
307  		if (strncmp(line, "ko:", 3) == 0) {
308  			retcode = EPKG_FATAL;
309  			goto out;
310  		}
311  	}
312  
313  out:
314  	free(url_to_free);
315  	free(line);
316  	return (retcode);
317  }
318  
319  int
320  tcp_open(struct pkg_repo *repo, struct fetch_item *fi)
321  {
322  	return (pkgprotocol_open(repo, fi, tcp_connect));
323  }
324  
325  int
326  ssh_open(struct pkg_repo *repo, struct fetch_item *fi)
327  {
328  	return (pkgprotocol_open(repo, fi, ssh_connect));
329  }
330  
331  static int
332  tcp_close(void *data)
333  {
334  	struct pkg_repo *repo = (struct pkg_repo *)data;
335  
336  	write(repo->sshio.out, "quit\n", 5);
337  	close(repo->sshio.out);
338  	close(repo->sshio.in);
339  	repo->fh = NULL;
340  	return (0);
341  }
342  
343  static int
344  ssh_close(void *data)
345  {
346  	struct pkg_repo *repo = (struct pkg_repo *)data;
347  	int pstat;
348  
349  	write(repo->sshio.out, "quit\n", 5);
350  
351  	while (waitpid(repo->sshio.pid, &pstat, 0) == -1) {
352  		if (errno != EINTR)
353  			return (EPKG_FATAL);
354  	}
355  	close(repo->sshio.out);
356  	close(repo->sshio.in);
357  
358  	repo->fh = NULL;
359  
360  	return (WEXITSTATUS(pstat));
361  }
362  
363  static int
364  ssh_writev(int fd, struct iovec *iov, int iovcnt, int64_t tmout)
365  {
366  	struct timespec now, timeout, delta;
367  	struct pollfd pfd;
368  	ssize_t wlen, total;
369  	int deltams;
370  	struct msghdr msg;
371  
372  	memset(&pfd, 0, sizeof pfd);
373  
374  	if (tmout > 0) {
375  		pfd.fd = fd;
376  		pfd.events = POLLOUT | POLLERR;
377  		clock_gettime(CLOCK_REALTIME, &timeout);
378  		timeout.tv_sec += tmout;
379  	}
380  
381  	total = 0;
382  	while (iovcnt > 0) {
383  		while (tmout && pfd.revents == 0) {
384  			clock_gettime(CLOCK_REALTIME, &now);
385  			if (!timespeccmp(&timeout, &now, >)) {
386  				errno = ETIMEDOUT;
387  				return (-1);
388  			}
389  			timespecsub(&timeout, &now, &delta);
390  			deltams = delta.tv_sec * 1000 +
391  				delta.tv_nsec / 1000000;
392  			errno = 0;
393  			pfd.revents = 0;
394  			while (poll(&pfd, 1, deltams) == -1) {
395  				if (errno == EINTR)
396  					continue;
397  
398  				return (-1);
399  			}
400  		}
401  		errno = 0;
402  		memset(&msg, 0, sizeof(msg));
403  		msg.msg_iov = iov;
404  		msg.msg_iovlen = iovcnt;
405  
406  		wlen = sendmsg(fd, &msg, 0);
407  		if (wlen == 0) {
408  			errno = ECONNRESET;
409  			return (-1);
410  		}
411  		else if (wlen < 0)
412  			return (-1);
413  
414  		total += wlen;
415  
416  		while (iovcnt > 0 && wlen >= (ssize_t)iov->iov_len) {
417  			wlen -= iov->iov_len;
418  			iov++;
419  			iovcnt--;
420  		}
421  
422  		if (iovcnt > 0) {
423  			iov->iov_len -= wlen;
424  			iov->iov_base = __DECONST(char *, iov->iov_base) + wlen;
425  		}
426  	}
427  	return (total);
428  }
429  
430  static int
431  ssh_write(void *data, const char *buf, int l)
432  {
433  	struct pkg_repo *repo = (struct pkg_repo *)data;
434  	struct iovec iov;
435  
436  	iov.iov_base = __DECONST(char *, buf);
437  	iov.iov_len = l;
438  
439  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> writing data");
440  
441  	return (ssh_writev(repo->sshio.out, &iov, 1, repo->fetcher->timeout));
442  }
443  
444  static int
445  ssh_read(void *data, char *buf, int len)
446  {
447  	struct pkg_repo *repo = (struct pkg_repo *) data;
448  	struct timespec now, timeout, delta;
449  	struct pollfd pfd;
450  	ssize_t rlen;
451  	int deltams;
452  
453  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> start reading");
454  
455  	if (repo->fetcher->timeout > 0) {
456  		clock_gettime(CLOCK_REALTIME, &timeout);
457  		timeout.tv_sec += repo->fetcher->timeout;
458  	}
459  
460  	deltams = -1;
461  	memset(&pfd, 0, sizeof pfd);
462  	pfd.fd = repo->sshio.in;
463  	pfd.events = POLLIN | POLLERR;
464  
465  	for (;;) {
466  		rlen = read(pfd.fd, buf, len);
467  		pkg_dbg(PKG_DBG_FETCH, 1, "SSH> read %jd", (intmax_t)rlen);
468  		if (rlen >= 0) {
469  			break;
470  		} else if (rlen == -1) {
471  			if (errno == EINTR)
472  				continue;
473  			if (errno != EAGAIN) {
474  				pkg_emit_errno("timeout", "ssh");
475  				return (-1);
476  			}
477  		}
478  
479  		/* only EAGAIN should get here */
480  		if (repo->fetcher->timeout > 0) {
481  			clock_gettime(CLOCK_REALTIME, &now);
482  			if (!timespeccmp(&timeout, &now, >)) {
483  				errno = ETIMEDOUT;
484  				return (-1);
485  			}
486  			timespecsub(&timeout, &now, &delta);
487  			deltams = delta.tv_sec * 1000 +
488  			    delta.tv_nsec / 1000000;
489  		}
490  
491  		errno = 0;
492  		pfd.revents = 0;
493  		pkg_dbg(PKG_DBG_FETCH, 2, "SSH> begin poll()");
494  		if (poll(&pfd, 1, deltams) < 0) {
495  			if (errno == EINTR)
496  				continue;
497  			return (-1);
498  		}
499  		pkg_dbg(PKG_DBG_FETCH, 2, "SSH> end poll()");
500  
501  	}
502  
503  	pkg_dbg(PKG_DBG_FETCH, 1, "SSH> have read %jd bytes", (intmax_t)rlen);
504  
505  	return (rlen);
506  }