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 }