/* $NetBSD: ftpio.c,v 1.32 2001/07/15 00:34:14 hubertf Exp $ */ #include #ifndef lint __RCSID("$NetBSD: ftpio.c,v 1.32 2001/07/15 00:34:14 hubertf Exp $"); #endif /* * Copyright (c) 1999 Hubert Feyrer. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by Hubert Feyrer for * the NetBSD Project. * 4. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef EXPECT_DEBUG #include #endif #include "../lib/lib.h" /* * Names of environment variables used to pass things to * subprocesses, for connection caching. */ #define PKG_FTPIO_COMMAND "PKG_FTPIO_COMMAND" #define PKG_FTPIO_ANSWER "PKG_FTPIO_ANSWER" #define PKG_FTPIO_CNT "PKG_FTPIO_CNT" #define PKG_FTPIO_CURRENTHOST "PKG_FTPIO_CURRENTHOST" #define PKG_FTPIO_CURRENTDIR "PKG_FTPIO_CURRENTDIR" #undef STANDALONE /* define for standalone debugging */ /* File descriptors */ typedef struct { int command; int answer; } fds; #if EXPECT_DEBUG static int expect_debug = 1; #endif /* EXPECT_DEBUG */ #ifdef STANDALONE int Verbose=1; #endif static int needclose=0; static int ftp_started=0; static fds ftpio; static int ftp_pid; /* * expect "str" (a regular expression) on file descriptor "fd", storing * the FTP return code of the command in the integer "ftprc". The "str" * string is expected to match some FTP return codes after a '\n', e.g. * "\n(550|226).*\n" */ static int expect(int fd, const char *str, int *ftprc) { int rc; char buf[90]; #if EXPECT_DEBUG char *vstr; #endif /* EXPECT_DEBUG */ regex_t rstr; int done; struct timeval timeout; int retval; regmatch_t pmatch; int verbose_expect=0; #if EXPECT_DEBUG vstr=malloc(2*sizeof(buf)); if (vstr == NULL) err(1, "expect: malloc() failed"); strvis(vstr, str, VIS_NL|VIS_SAFE|VIS_CSTYLE); #endif /* EXPECT_DEBUG */ if (regcomp(&rstr, str, REG_EXTENDED) != 0) err(1, "expect: regcomp() failed"); #if EXPECT_DEBUG if (expect_debug) printf("expecting \"%s\" on fd %d ...\n", vstr, fd); #endif /* EXPECT_DEBUG */ if(0) setbuf(stdout, NULL); memset(buf, '\n', sizeof(buf)); timeout.tv_sec=10*60; /* seconds until next message from tar */ timeout.tv_usec=0; done=0; retval=0; while(!done) { fd_set fdset; FD_ZERO(&fdset); FD_SET(fd, &fdset); rc = select(FD_SETSIZE, &fdset, NULL, NULL, &timeout); switch (rc) { case -1: if (errno == EINTR) break; warn("expect: select() failed (probably ftp died because of bad args)"); done = 1; retval = -1; break; case 0: warnx("expect: select() timeout"); /* need to send ftp coprocess SIGINT to make it stop * downloading into dir that we'll blow away in a second */ kill(ftp_pid, SIGINT); /* Wait until ftp coprocess is responsive again * XXX Entering recursion here! */ rc = ftp_cmd("cd .\n", "\n(550|250).*\n"); if (rc != 250) { /* now we have a really good reason to bail out ;) */ } /* ftp is at command prompt again, and will wait for our * next command. If we were downloading, we can now safely * continue and remove the dir that the tar command was * expanding to */ done = 1; /* hope that's ok */ retval = -1; break; default: rc=read(fd,&buf[sizeof(buf)-1],1); if (verbose_expect) putchar(buf[sizeof(buf)-1]); #if EXPECT_DEBUG { char *v=malloc(2*sizeof(buf)); strvis(v, buf, VIS_NL|VIS_SAFE|VIS_CSTYLE); if (expect_debug) printf("expect=<%s>, buf=<%*s>\n", vstr, strlen(v), v); free(v); } #endif /* EXPECT_DEBUG */ if (regexec(&rstr, buf, 1, &pmatch, 0) == 0) { #if EXPECT_DEBUG if (expect_debug) printf("Gotcha -> %s!\n", buf+pmatch.rm_so+1); fflush(stdout); #endif /* EXPECT_DEBUG */ if (ftprc && isdigit(buf[pmatch.rm_so+1])) *ftprc = atoi(buf+pmatch.rm_so+1); done=1; retval=0; } memmove(buf, buf+1, sizeof(buf)-1); /* yes, this is non-performant */ break; } } #if EXPECT_DEBUG printf("done.\n"); if (str) free(vstr); #endif /* EXPECT_DEBUG */ return retval; } /* * send a certain ftp-command "cmd" to our FTP coprocess, and wait for * "expectstr" to be returned. Return numeric FTP return code or -1 * in case of an error (usually expect() timeout) */ int ftp_cmd(const char *cmd, const char *expectstr) { int rc=0, verbose_ftp=0; int len; if (Verbose) verbose_ftp=1; if (verbose_ftp) fprintf(stderr, "\nftp> %s", cmd); fflush(stdout); len = write(ftpio.command, cmd, strlen(cmd)); if (len == strlen(cmd)) { if (expectstr) { /* set "rc" to the FTP error code: */ if (expect(ftpio.answer, expectstr, &rc) == -1) rc = -1; /* some error occured */ } } else { if (Verbose) warn("short write"); } return rc; } /* * Really fire up FTP coprocess */ static int setupCoproc(const char *base) { int command_pipe[2]; int answer_pipe[2]; int rc1, rc2; char buf[20]; rc1 = pipe(command_pipe); rc2 = pipe(answer_pipe); if(rc1==-1 || rc2==-1) { warn("setupCoproc: pipe() failed"); return -1; } rc1 = fork(); switch (rc1) { case -1: /* Error */ warn("setupCoproc: fork() failed"); return -1; break; case 0: /* Child */ (void) close(command_pipe[1]); dup2(command_pipe[0], 0); (void) close(command_pipe[0]); (void) close(answer_pipe[0]); dup2(answer_pipe[1], 1); (void) close(answer_pipe[1]); setbuf(stdout, NULL); if (Verbose) fprintf(stderr, "ftp -detv %s\n", base); rc1 = execl("/usr/bin/ftp", "ftp", "-detv", base, NULL); warn("setupCoproc: execl() failed"); exit(1); break; default: /* Parent */ (void) close(command_pipe[0]); (void) close(answer_pipe[1]); (void) snprintf(buf, sizeof(buf), "%d", command_pipe[1]); setenv(PKG_FTPIO_COMMAND, buf, 1); (void) snprintf(buf, sizeof(buf), "%d", answer_pipe[0]); setenv(PKG_FTPIO_ANSWER, buf, 1); ftpio.command = command_pipe[1]; ftpio.answer = answer_pipe[0]; ftp_pid = rc1; /* to ^C transfers */ fcntl(ftpio.command, F_SETFL, O_NONBLOCK); fcntl(ftpio.answer , F_SETFL, O_NONBLOCK); break; } return 0; } /* * Dummy signal handler to detect if the ftp(1) coprocess or * and of the processes of the tar/gzip pipeline dies. */ static void sigchld_handler (int n) { /* Make select(2) return EINTR */ } /* * SIGPIPE only happens when there's something wrong with the FTP * coprocess. In that case, set mark to not try to close shut down * the coprocess. */ static void sigpipe_handler(int n) { /* aparently our ftp companion died */ if (Verbose) fprintf(stderr, "SIGPIPE!\n"); needclose = 0; } /* * Close the FTP coprocess' current connection, but * keep the process itself alive. */ void ftp_stop(void) { #if defined(__svr4__) && defined(__sun__) char env[BUFSIZ]; #endif char *tmp1, *tmp2; if (!ftp_started) return; tmp1=getenv(PKG_FTPIO_COMMAND); tmp2=getenv(PKG_FTPIO_ANSWER); /* (Only) the last one closes the link */ if (tmp1 != NULL && tmp2 != NULL) { if (needclose) ftp_cmd("close\n", "\n(221 .*|Not connected.)\n"); (void) close(ftpio.command); (void) close(ftpio.answer); } #if defined(__svr4__) && defined(__sun__) (void) snprintf(env, sizeof(env), "%s=", PKG_FTPIO_COMMAND); putenv(env); (void) snprintf(env, sizeof(env), "%s=", PKG_FTPIO_ANSWER); putenv(env); #else unsetenv(PKG_FTPIO_COMMAND); unsetenv(PKG_FTPIO_ANSWER); #endif } /* * (Start and re-)Connect the FTP coprocess to some host/dir. * If the requested host/dir is different than the one that the * coprocess is currently at, close first. */ int ftp_start(char *base) { char *tmp1, *tmp2; int rc; char newHost[256]; char newDir[1024]; char *currentHost=getenv(PKG_FTPIO_CURRENTHOST); char *currentDir=getenv(PKG_FTPIO_CURRENTDIR); fileURLHost(base, newHost, sizeof(newHost)); strcpy(newDir, strchr(base+URLlength(base), '/') + 1); if (currentHost && currentDir && ( strcmp(newHost, currentHost) != 0 || strcmp(newDir, currentDir) != 0)) { /* could handle new dir case better here, w/o reconnect */ if (Verbose) { printf("ftp_start: new host or dir, stopping previous connect...\n"); printf("currentHost='%s', newHost='%s'\n", currentHost, newHost); printf("currentDir='%s', newDir='%s'\n", currentDir, newDir); } ftp_stop(); if (Verbose) printf("ftp stopped\n"); } setenv(PKG_FTPIO_CURRENTHOST, newHost, 1); /* need to update this in the environment */ setenv(PKG_FTPIO_CURRENTDIR, newDir, 1); /* for subprocesses to have this available */ tmp1=getenv(PKG_FTPIO_COMMAND); tmp2=getenv(PKG_FTPIO_ANSWER); if(tmp1==NULL || tmp2==NULL || *tmp1=='\0' || *tmp2=='\0') { /* no FTP coprocess running yet */ if (Verbose) printf("Spawning FTP coprocess\n"); rc = setupCoproc(base); if (rc == -1) { warnx("setupCoproc() failed"); return -1; } needclose=1; signal(SIGPIPE, sigpipe_handler); signal(SIGCHLD, sigchld_handler); if ((expect(ftpio.answer, "\n(221|250|221|550).*\n", &rc) != 0) || rc != 250) { warnx("expect1 failed, rc=%d", rc); return -1; } /* lukemftp now issues a CWD for each part of the path * and will return a code for each of them. No idea how to * deal with that other than to issue a 'prompt off' to * get something that we can wait for and that does NOT * look like a CWD command's output */ rc = ftp_cmd("prompt off\n", "\n(Interactive mode off|221).*\n"); if ((rc == 221) || (rc == -1)) { /* something is wrong */ ftp_started=1; /* not really, but for ftp_stop() */ ftp_stop(); warnx("prompt failed - wrong dir?"); return -1; } ftp_started=1; } else { /* get FDs of our coprocess */ ftpio.command = dup(atoi(tmp1)); ftpio.answer = dup(atoi(tmp2)); if (Verbose) printf("Reusing FDs %s/%s for communication to FTP coprocess\n", tmp1, tmp2); fcntl(ftpio.command, F_SETFL, O_NONBLOCK); fcntl(ftpio.answer , F_SETFL, O_NONBLOCK); } return 0; } /* * Expand the given wildcard URL "wildcardurl" if possible, and store the * expanded value into "expandedurl". return 0 if successful, -1 else. */ int expandURL(char *expandedurl, const char *wildcardurl) { char *pkg; int rc; char base[FILENAME_MAX]; pkg=strrchr(wildcardurl, '/'); if (pkg == NULL){ warnx("expandURL: no '/' in url %s?!", wildcardurl); return -1; } (void) snprintf(base, sizeof(base), "%*.*s/", (int)(pkg-wildcardurl), (int)(pkg-wildcardurl), wildcardurl); pkg++; rc = ftp_start(base); if (rc == -1) { warnx("ftp_start() failed"); return -1; /* error */ } /* for a given wildcard URL, find the best matching pkg */ { char *s, buf[FILENAME_MAX]; char tmpname[FILENAME_MAX]; char best[FILENAME_MAX]; int tfd; strcpy(tmpname, "/var/tmp/pkg.XXXXXX"); tfd=mkstemp(tmpname); if (tfd == -1) { warnx("Cannot generate temp file for ftp(1)'s nlist output"); return -1; /* error */ } close(tfd); /* We don't need the file descriptor, but will use the file in a second */ s=strpbrk(pkg, "<>[]?*{"); /* Could leave out "[]?*" here; * ftp(1) is not that stupid */ if (!s) { /* This should only happen when getting here with (only) a package * name specified to pkg_add, and PKG_PATH containing some URL. */ (void) snprintf(buf, sizeof(buf), "nlist %s %s\n", pkg, tmpname); } else { /* replace possible version(wildcard) given with "-*". * we can't use the pkg wildcards here as dewey compare * and alternates won't be handled by ftp(1); sort * out later, using pmatch() */ (void) snprintf(buf, sizeof(buf), "nlist %*.*s*.t[bg]z %s\n", (int)(s-pkg), (int)(s-pkg), pkg, tmpname); } rc = ftp_cmd(buf, "\n(550|226).*\n"); /* catch errors */ if (rc != 226) { if (Verbose) warnx("nlist failed!"); unlink(tmpname); /* remove clutter */ return -1; } /* Sync - don't remove */ rc = ftp_cmd("cd .\n", "\n(550|250).*\n"); if (rc != 250) { warnx("chdir failed!"); unlink(tmpname); /* remove clutter */ return -1; } best[0]='\0'; if (access(tmpname, R_OK)==0) { int matches; FILE *f; char filename[FILENAME_MAX]; f=fopen(tmpname, "r"); if (f == NULL) { warn("fopen"); unlink(tmpname); /* remove clutter */ return -1; } matches=0; /* The following loop is basically the same as the readdir() loop * in findmatchingname() */ while (fgets(filename, sizeof(buf), f)) { /* * We need to stripp of any .t[bg]z etc. * suffix here */ char s_filename[FILENAME_MAX]; char s_pkg[FILENAME_MAX]; filename[strlen(filename)-1] = '\0'; strip_txz(s_filename, NULL, filename); strip_txz(s_pkg, NULL, pkg); if (pmatch(s_pkg, s_filename)) { matches++; /* compare findbestmatchingname() */ findbestmatchingname_fn(filename, best); } } (void) fclose(f); if (matches == 0) warnx("nothing appropriate found\n"); } unlink(tmpname); if (best[0] != '\0') { if (Verbose) printf("best match: '%s%s'\n", base, best); sprintf(expandedurl, "%s%s", base, best); } } return 0; } /* * extract the given (expanded) URL "url" to the given directory "dir" * return -1 on error, 0 else; */ int unpackURL(const char *url, const char *dir) { char *pkg; int rc; char base[FILENAME_MAX]; char pkg_path[FILENAME_MAX]; { /* Verify if the url is really ok */ int rc; char exp[FILENAME_MAX]; rc=expandURL(exp, url); if (rc == -1) { warnx("unpackURL: verification expandURL failed"); return -1; } if (strcmp(exp, url) != 0) { warnx("unpackURL: verificatinon expandURL failed, '%s'!='%s'", exp, url); return -1; } } pkg=strrchr(url, '/'); if (pkg == NULL){ warnx("unpackURL: no '/' in url %s?!", url); return -1; } (void) snprintf(base, sizeof(base), "%*.*s/", (int)(pkg-url), (int)(pkg-url), url); (void) snprintf(pkg_path, sizeof(pkg_path), "%*.*s", (int)(pkg-url), (int)(pkg-url), url); /* no trailing '/' */ pkg++; /* Leave a hint for any depending pkgs that may need it */ if (getenv("PKG_PATH") == NULL) { setenv("PKG_PATH", pkg_path, 1); printf("setenv PKG_PATH='%s'\n",pkg_path); } rc = ftp_start(base); if (rc == -1) { warnx("ftp_start() failed"); return -1; /* error */ } { char cmd[1024]; if (Verbose) printf("unpackURL '%s' to '%s'\n", url, dir); /* yes, this is gross, but needed for borken ftp(1) */ (void) snprintf(cmd, sizeof(cmd), "get %s \"| ( cd %s ; gunzip 2>/dev/null | tar -%sx -f - | tee /dev/stderr )\"\n", pkg, dir, Verbose?"vv":""); rc = ftp_cmd(cmd, "\n(226|550).*\n"); if (rc != 226) { warnx("Cannot fetch file (%d!=226)!", rc); return -1; } } return 0; } #if 0 /* * Some misc stuff not needed yet, but maybe later */ int miscstuff(const char *url) { char *pkg; int rc; char base[FILENAME_MAX]; pkg=strrchr(url, '/'); if (pkg == NULL){ warnx("miscstuff: no '/' in url %s?!", url); return -1; } (void) snprintf(base, sizeof(base), "%*.*s/", (int)(pkg-url), (int)(pkg-url), url); pkg++; rc = ftp_start(base); if (rc == -1) { warnx("ftp_start() failed"); return -1; /* error */ } /* basic operation */ if (0) { rc = ftp_cmd("cd ../All\n", "\n(550|250).*\n"); if (rc != 250) { warnx("chdir failed!"); return -1; } } /* get and extract a file to tmpdir */ if (0) { char cmd[256]; char tmpdir[256]; (void) snprintf(tmpdir, sizeof(tmpdir), "/var/tmp/dir%s", (getenv(PKG_FTPIO_CNT))?getenv(PKG_FTPIO_CNT):""); mkdir(tmpdir, 0755); /* yes, this is gross, but needed for borken ftp(1) */ (void) snprintf(cmd, sizeof(cmd), "get xpmroot-1.01.tgz \"| ( cd %s ; gunzip 2>/dev/null | tar -vvx -f - )\"\n", tmpdir); rc = ftp_cmd(cmd, "\n(226|550).*\n"); if (rc != 226) { warnx("Cannot fetch file (%d != 226)!", rc); return -1; } } /* check if one more file(s) exist */ if (0) { char buf[FILENAME_MAX]; (void) snprintf(buf, sizeof(buf), "nlist %s /var/tmp/xxx\n", pkg); rc = ftp_cmd(buf, "\n(226|550).*\n"); /* catch errors */ if (rc != 226) { if (Verbose) warnx("nlist failed!"); return -1; } /* Sync - don't remove */ rc = ftp_cmd("cd .\n", "\n(550|250).*\n"); if (rc != 250) { warnx("chdir failed!"); return -1; } if (access("/var/tmp/xxx", R_OK)==0) { system("cat /var/tmp/xxx"); { /* count lines - >0 -> fexists() == true */ int len, count; FILE *f; f=fopen("/var/tmp/xxx", "r"); if (f == NULL) { warn("fopen"); return -1; } count=0; while (fgetln(f, &len)) count++; (void) fclose(f); printf("#lines = %d\n", count); } } else printf("NO MATCH\n"); unlink("/var/tmp/xxx"); } /* for a given wildcard URL, find the best matching pkg */ /* spawn child - like pkg_add'ing another package */ /* Here's where the connection caching kicks in */ #if 0 if (0) { char *s, buf[FILENAME_MAX]; if ((s=getenv(PKG_FTPIO_CNT)) && atoi(s)>0){ (void) snprintf(buf, sizeof(buf),"%d", atoi(s)-1); setenv(PKG_FTPIO_CNT, buf, 1); (void) snprintf(buf, sizeof(buf), "%s \"%s/%s\"", argv0, url, pkg); printf("%s>>> %s\n", s, buf); system(buf); } } #endif return 0; } #endif #ifdef STANDALONE static void usage(void) { errx(1, "Usage: foo [-v] ftp://-pattern\n"); } int main(int argc, char *argv[]) { int rc, ch; char *argv0 = argv[0]; while ((ch = getopt(argc, argv, "v")) != -1) { switch (ch) { case 'v': Verbose=1; break; default: usage(); } } argc -= optind; argv += optind; if (argc<1) usage(); while(argv[0] != NULL) { char newurl[FILENAME_MAX]; printf("Expand %s:\n", argv[0]); rc = expandURL(newurl, argv[0]); if (rc==-1) warnx("Cannot expand %s", argv[0]); else printf("Expanded URL: %s\n", newurl); /* test out connection caching */ if (1) { char *s, buf[FILENAME_MAX]; if ((s=getenv(PKG_FTPIO_CNT)) && atoi(s)>0){ (void) snprintf(buf, sizeof(buf),"%d", atoi(s)-1); setenv(PKG_FTPIO_CNT, buf, 1); (void) snprintf(buf, sizeof(buf), "%s -v '%s'", argv0, argv[0]); printf("%s>>> %s\n", s, buf); system(buf); } } printf("\n\n\n"); argv++; } ftp_stop(); return 0; } #endif /* STANDALONE */