1 /* $Id: main.c,v 1.44 2019/04/04 04:19:54 bket Exp $ */ 2 /* 3 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv> 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 #include <sys/stat.h> 18 #include <sys/socket.h> 19 #include <sys/wait.h> 20 21 #include <assert.h> 22 #include <err.h> 23 #include <getopt.h> 24 #include <stdint.h> 25 #include <stdio.h> 26 #include <stdlib.h> 27 #include <string.h> 28 #include <unistd.h> 29 30 #include "extern.h" 31 32 /* 33 * A remote host is has a colon before the first path separator. 34 * This works for rsh remote hosts (host:/foo/bar), implicit rsync 35 * remote hosts (host::/foo/bar), and explicit (rsync://host/foo). 36 * Return zero if local, non-zero if remote. 37 */ 38 static int 39 fargs_is_remote(const char *v) 40 { 41 size_t pos; 42 43 pos = strcspn(v, ":/"); 44 return v[pos] == ':'; 45 } 46 47 /* 48 * Test whether a remote host is specifically an rsync daemon. 49 * Return zero if not, non-zero if so. 50 */ 51 static int 52 fargs_is_daemon(const char *v) 53 { 54 size_t pos; 55 56 if (strncasecmp(v, "rsync://", 8) == 0) 57 return 1; 58 59 pos = strcspn(v, ":/"); 60 return v[pos] == ':' && v[pos + 1] == ':'; 61 } 62 63 /* 64 * Take the command-line filenames (e.g., rsync foo/ bar/ baz/) and 65 * determine our operating mode. 66 * For example, if the first argument is a remote file, this means that 67 * we're going to transfer from the remote to the local. 68 * We also make sure that the arguments are consistent, that is, if 69 * we're going to transfer from the local to the remote, that no 70 * filenames for the local transfer indicate remote hosts. 71 * Always returns the parsed and sanitised options. 72 */ 73 static struct fargs * 74 fargs_parse(size_t argc, char *argv[], struct opts *opts) 75 { 76 struct fargs *f = NULL; 77 char *cp, *ccp; 78 size_t i, j, len = 0; 79 80 /* Allocations. */ 81 82 if ((f = calloc(1, sizeof(struct fargs))) == NULL) 83 err(1, "calloc"); 84 85 f->sourcesz = argc - 1; 86 if ((f->sources = calloc(f->sourcesz, sizeof(char *))) == NULL) 87 err(1, "calloc"); 88 89 for (i = 0; i < argc - 1; i++) 90 if ((f->sources[i] = strdup(argv[i])) == NULL) 91 err(1, "strdup"); 92 93 if ((f->sink = strdup(argv[i])) == NULL) 94 err(1, "strdup"); 95 96 /* 97 * Test files for its locality. 98 * If the last is a remote host, then we're sending from the 99 * local to the remote host ("sender" mode). 100 * If the first, remote to local ("receiver" mode). 101 * If neither, a local transfer in sender style. 102 */ 103 104 f->mode = FARGS_SENDER; 105 106 if (fargs_is_remote(f->sink)) { 107 f->mode = FARGS_SENDER; 108 if ((f->host = strdup(f->sink)) == NULL) 109 err(1, "strdup"); 110 } 111 112 if (fargs_is_remote(f->sources[0])) { 113 if (f->host != NULL) 114 errx(1, "both source and destination cannot be remote files"); 115 f->mode = FARGS_RECEIVER; 116 if ((f->host = strdup(f->sources[0])) == NULL) 117 err(1, "strdup"); 118 } 119 120 if (f->host != NULL) { 121 if (strncasecmp(f->host, "rsync://", 8) == 0) { 122 /* rsync://host[:port]/module[/path] */ 123 f->remote = 1; 124 len = strlen(f->host) - 8 + 1; 125 memmove(f->host, f->host + 8, len); 126 if ((cp = strchr(f->host, '/')) == NULL) 127 errx(1, "rsync protocol requires a module name"); 128 *cp++ = '\0'; 129 f->module = cp; 130 if ((cp = strchr(f->module, '/')) != NULL) 131 *cp = '\0'; 132 if ((cp = strchr(f->host, ':'))) { 133 /* host:port --> extract port */ 134 *cp++ = '\0'; 135 opts->port = cp; 136 } 137 } else { 138 /* host:[/path] */ 139 cp = strchr(f->host, ':'); 140 assert(cp != NULL); 141 *cp++ = '\0'; 142 if (*cp == ':') { 143 /* host::module[/path] */ 144 f->remote = 1; 145 f->module = ++cp; 146 cp = strchr(f->module, '/'); 147 if (cp != NULL) 148 *cp = '\0'; 149 } 150 } 151 if ((len = strlen(f->host)) == 0) 152 errx(1, "empty remote host"); 153 if (f->remote && strlen(f->module) == 0) 154 errx(1, "empty remote module"); 155 } 156 157 /* Make sure we have the same "hostspec" for all files. */ 158 159 if (!f->remote) { 160 if (f->mode == FARGS_SENDER) 161 for (i = 0; i < f->sourcesz; i++) { 162 if (!fargs_is_remote(f->sources[i])) 163 continue; 164 errx(1, 165 "remote file in list of local sources: %s", 166 f->sources[i]); 167 } 168 if (f->mode == FARGS_RECEIVER) 169 for (i = 0; i < f->sourcesz; i++) { 170 if (fargs_is_remote(f->sources[i]) && 171 !fargs_is_daemon(f->sources[i])) 172 continue; 173 if (fargs_is_daemon(f->sources[i])) 174 errx(1, "remote daemon in list of " 175 "remote sources: %s", 176 f->sources[i]); 177 errx(1, "local file in list of remote sources: %s", 178 f->sources[i]); 179 } 180 } else { 181 if (f->mode != FARGS_RECEIVER) 182 errx(1, "sender mode for remote " 183 "daemon receivers not yet supported"); 184 for (i = 0; i < f->sourcesz; i++) { 185 if (fargs_is_daemon(f->sources[i])) 186 continue; 187 errx(1, "non-remote daemon file " 188 "in list of remote daemon sources: " 189 "%s", f->sources[i]); 190 } 191 } 192 193 /* 194 * If we're not remote and a sender, strip our hostname. 195 * Then exit if we're a sender or a local connection. 196 */ 197 198 if (!f->remote) { 199 if (f->host == NULL) 200 return f; 201 if (f->mode == FARGS_SENDER) { 202 assert(f->host != NULL); 203 assert(len > 0); 204 j = strlen(f->sink); 205 memmove(f->sink, f->sink + len + 1, j - len); 206 return f; 207 } else if (f->mode != FARGS_RECEIVER) 208 return f; 209 } 210 211 /* 212 * Now strip the hostnames from the remote host. 213 * rsync://host/module/path -> module/path 214 * host::module/path -> module/path 215 * host:path -> path 216 * Also make sure that the remote hosts are the same. 217 */ 218 219 assert(f->host != NULL); 220 assert(len > 0); 221 222 for (i = 0; i < f->sourcesz; i++) { 223 cp = f->sources[i]; 224 j = strlen(cp); 225 if (f->remote && 226 strncasecmp(cp, "rsync://", 8) == 0) { 227 /* rsync://path */ 228 cp += 8; 229 if ((ccp = strchr(cp, ':'))) /* skip :port */ 230 *ccp = '\0'; 231 if (strncmp(cp, f->host, len) || 232 (cp[len] != '/' && cp[len] != '\0')) 233 errx(1, "different remote host: %s", 234 f->sources[i]); 235 memmove(f->sources[i], 236 f->sources[i] + len + 8 + 1, 237 j - len - 8); 238 } else if (f->remote && strncmp(cp, "::", 2) == 0) { 239 /* ::path */ 240 memmove(f->sources[i], 241 f->sources[i] + 2, j - 1); 242 } else if (f->remote) { 243 /* host::path */ 244 if (strncmp(cp, f->host, len) || 245 (cp[len] != ':' && cp[len] != '\0')) 246 errx(1, "different remote host: %s", 247 f->sources[i]); 248 memmove(f->sources[i], f->sources[i] + len + 2, 249 j - len - 1); 250 } else if (cp[0] == ':') { 251 /* :path */ 252 memmove(f->sources[i], f->sources[i] + 1, j); 253 } else { 254 /* host:path */ 255 if (strncmp(cp, f->host, len) || 256 (cp[len] != ':' && cp[len] != '\0')) 257 errx(1, "different remote host: %s", 258 f->sources[i]); 259 memmove(f->sources[i], 260 f->sources[i] + len + 1, j - len); 261 } 262 } 263 264 return f; 265 } 266 267 int 268 main(int argc, char *argv[]) 269 { 270 struct opts opts; 271 pid_t child; 272 int fds[2], sd, rc, c, st, i; 273 struct sess sess; 274 struct fargs *fargs; 275 char **args; 276 struct option lopts[] = { 277 { "port", required_argument, NULL, 3 }, 278 { "rsh", required_argument, NULL, 'e' }, 279 { "rsync-path", required_argument, NULL, 1 }, 280 { "sender", no_argument, &opts.sender, 1 }, 281 { "server", no_argument, &opts.server, 1 }, 282 { "dry-run", no_argument, &opts.dry_run, 1 }, 283 { "version", no_argument, NULL, 2 }, 284 { "archive", no_argument, NULL, 'a' }, 285 { "help", no_argument, NULL, 'h' }, 286 { "compress", no_argument, NULL, 'z' }, 287 { "del", no_argument, &opts.del, 1 }, 288 { "delete", no_argument, &opts.del, 1 }, 289 { "devices", no_argument, &opts.devices, 1 }, 290 { "no-devices", no_argument, &opts.devices, 0 }, 291 { "group", no_argument, &opts.preserve_gids, 1 }, 292 { "no-group", no_argument, &opts.preserve_gids, 0 }, 293 { "links", no_argument, &opts.preserve_links, 1 }, 294 { "no-links", no_argument, &opts.preserve_links, 0 }, 295 { "owner", no_argument, &opts.preserve_uids, 1 }, 296 { "no-owner", no_argument, &opts.preserve_uids, 0 }, 297 { "perms", no_argument, &opts.preserve_perms, 1 }, 298 { "no-perms", no_argument, &opts.preserve_perms, 0 }, 299 { "numeric-ids", no_argument, &opts.numeric_ids, 1 }, 300 { "recursive", no_argument, &opts.recursive, 1 }, 301 { "no-recursive", no_argument, &opts.recursive, 0 }, 302 { "specials", no_argument, &opts.specials, 1 }, 303 { "no-specials", no_argument, &opts.specials, 0 }, 304 { "times", no_argument, &opts.preserve_times, 1 }, 305 { "no-times", no_argument, &opts.preserve_times, 0 }, 306 { "verbose", no_argument, &opts.verbose, 1 }, 307 { "no-verbose", no_argument, &opts.verbose, 0 }, 308 { NULL, 0, NULL, 0 }}; 309 310 /* Global pledge. */ 311 312 if (pledge("stdio unix rpath wpath cpath dpath inet fattr chown dns getpw proc exec unveil", 313 NULL) == -1) 314 err(1, "pledge"); 315 316 memset(&opts, 0, sizeof(struct opts)); 317 318 while ((c = getopt_long(argc, argv, "Dae:ghlnoprtvxz", lopts, NULL)) 319 != -1) { 320 switch (c) { 321 case 'D': 322 opts.devices = 1; 323 opts.specials = 1; 324 break; 325 case 'a': 326 opts.recursive = 1; 327 opts.preserve_links = 1; 328 opts.preserve_perms = 1; 329 opts.preserve_times = 1; 330 opts.preserve_gids = 1; 331 opts.preserve_uids = 1; 332 opts.devices = 1; 333 opts.specials = 1; 334 break; 335 case 'e': 336 opts.ssh_prog = optarg; 337 break; 338 case 'g': 339 opts.preserve_gids = 1; 340 break; 341 case 'l': 342 opts.preserve_links = 1; 343 break; 344 case 'n': 345 opts.dry_run = 1; 346 break; 347 case 'o': 348 opts.preserve_uids = 1; 349 break; 350 case 'p': 351 opts.preserve_perms = 1; 352 break; 353 case 'r': 354 opts.recursive = 1; 355 break; 356 case 't': 357 opts.preserve_times = 1; 358 break; 359 case 'v': 360 opts.verbose++; 361 break; 362 case 'x': 363 opts.one_file_system++; 364 break; 365 case 'z': 366 fprintf(stderr, "%s: -z not supported yet\n", getprogname()); 367 break; 368 case 0: 369 /* Non-NULL flag values (e.g., --sender). */ 370 break; 371 case 1: 372 opts.rsync_path = optarg; 373 break; 374 case 2: 375 fprintf(stderr, "openrsync: protocol version %u\n", 376 RSYNC_PROTOCOL); 377 exit(0); 378 case 3: 379 opts.port = optarg; 380 break; 381 case 'h': 382 default: 383 goto usage; 384 } 385 } 386 387 argc -= optind; 388 argv += optind; 389 390 /* FIXME: reference implementation rsync accepts this. */ 391 392 if (argc < 2) 393 goto usage; 394 395 if (opts.port == NULL) 396 opts.port = "rsync"; 397 398 /* 399 * This is what happens when we're started with the "hidden" 400 * --server option, which is invoked for the rsync on the remote 401 * host by the parent. 402 */ 403 404 if (opts.server) 405 exit(rsync_server(&opts, (size_t)argc, argv)); 406 407 /* 408 * Now we know that we're the client on the local machine 409 * invoking rsync(1). 410 * At this point, we need to start the client and server 411 * initiation logic. 412 * The client is what we continue running on this host; the 413 * server is what we'll use to connect to the remote and 414 * invoke rsync with the --server option. 415 */ 416 417 fargs = fargs_parse(argc, argv, &opts); 418 assert(fargs != NULL); 419 420 /* 421 * If we're contacting an rsync:// daemon, then we don't need to 422 * fork, because we won't start a server ourselves. 423 * Route directly into the socket code, unless a remote shell 424 * has explicitly been specified. 425 */ 426 427 if (fargs->remote && opts.ssh_prog == NULL) { 428 assert(fargs->mode == FARGS_RECEIVER); 429 if ((rc = rsync_connect(&opts, &sd, fargs)) == 0) 430 rc = rsync_socket(&opts, sd, fargs); 431 exit(rc); 432 } 433 434 /* Drop the dns/inet possibility. */ 435 436 if (pledge("stdio unix rpath wpath cpath dpath fattr chown getpw proc exec unveil", 437 NULL) == -1) 438 err(1, "pledge"); 439 440 /* Create a bidirectional socket and start our child. */ 441 442 if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0, fds) == -1) 443 err(1, "socketpair"); 444 445 switch ((child = fork())) { 446 case -1: 447 err(1, "fork"); 448 case 0: 449 close(fds[0]); 450 if (pledge("stdio exec", NULL) == -1) 451 err(1, "pledge"); 452 453 memset(&sess, 0, sizeof(struct sess)); 454 sess.opts = &opts; 455 456 if ((args = fargs_cmdline(&sess, fargs, NULL)) == NULL) { 457 ERRX1(&sess, "fargs_cmdline"); 458 _exit(1); 459 } 460 461 for (i = 0; args[i] != NULL; i++) 462 LOG2(&sess, "exec[%d] = %s", i, args[i]); 463 464 /* Make sure the child's stdin is from the sender. */ 465 if (dup2(fds[1], STDIN_FILENO) == -1) { 466 ERR(&sess, "dup2"); 467 _exit(1); 468 } 469 if (dup2(fds[1], STDOUT_FILENO) == -1) { 470 ERR(&sess, "dup2"); 471 _exit(1); 472 } 473 execvp(args[0], args); 474 _exit(1); 475 /* NOTREACHED */ 476 default: 477 close(fds[1]); 478 if (!fargs->remote) 479 rc = rsync_client(&opts, fds[0], fargs); 480 else 481 rc = rsync_socket(&opts, fds[0], fargs); 482 break; 483 } 484 485 /* 486 * If the client has an error and exits, the server may be 487 * sitting around waiting to get data while we waitpid(). 488 * So close the connection here so that they don't hang. 489 */ 490 491 if (rc) 492 close(fds[0]); 493 494 if (waitpid(child, &st, 0) == -1) 495 err(1, "waitpid"); 496 497 /* 498 * If we don't already have an error (rc == 0), then inherit the 499 * error code of rsync_server() if it has exited. 500 * If it hasn't exited, it overrides our return value. 501 */ 502 503 if (WIFEXITED(st) && rc == 0) 504 rc = WEXITSTATUS(st); 505 else if (!WIFEXITED(st)) 506 rc = 1; 507 508 exit(rc); 509 usage: 510 fprintf(stderr, "usage: %s" 511 " [-aDglnoprtvx] [-e program] [--del] [--numeric-ids]\n" 512 "\t[--port=portnumber] [--rsync-path=program] [--version]\n" 513 "\tsource ... directory\n", 514 getprogname()); 515 exit(1); 516 } 517