/* Internet FTP client (interactive user) * Copyright 1991 Phil Karn, KA9Q */ #include #include "global.h" #include "mbuf.h" #include "session.h" #include "cmdparse.h" #include "proc.h" #include "tty.h" #include "socket.h" #include "ftp.h" #include "ftpcli.h" #include "commands.h" #include "netuser.h" #include "dirutil.h" #include "internet.h" #define DIRBUF 256 static int doascii __ARGS((int argc,char *argv[],void *p)); static int dobatch __ARGS((int argc,char *argv[],void *p)); static int dobinary __ARGS((int argc,char *argv[],void *p)); static int doftpcd __ARGS((int argc,char *argv[],void *p)); static int doget __ARGS((int argc,char *argv[],void *p)); static int dohash __ARGS((int argc,char *argv[],void *p)); static int doverbose __ARGS((int argc,char *argv[],void *p)); static int dolist __ARGS((int argc,char *argv[],void *p)); static int dols __ARGS((int argc,char *argv[],void *p)); static int domkdir __ARGS((int argc,char *argv[],void *p)); static int domget __ARGS((int argc,char *argv[],void *p)); static int domput __ARGS((int argc,char *argv[],void *p)); static int doput __ARGS((int argc,char *argv[],void *p)); static int doquit __ARGS((int argc,char *argv[],void *p)); static int dormdir __ARGS((int argc,char *argv[],void *p)); static int dotype __ARGS((int argc,char *argv[],void *p)); static int getline __ARGS((struct session *sp,char *prompt,char *buf,int n)); static int getresp __ARGS((struct ftpcli *ftp,int mincode)); static long getsub __ARGS((struct ftpcli *ftp,char *command,char *remotename, char *localname)); static long putsub __ARGS((struct ftpcli *ftp,char *remotename,char *localname)); static void sendport __ARGS((int s,struct sockaddr_in *socket)); static char Notsess[] = "Not an FTP session!\n"; static struct cmds Ftpcmds[] = { "", donothing, 0, 0, NULLCHAR, "ascii", doascii, 0, 0, NULLCHAR, "batch", dobatch, 0, 0, NULLCHAR, "binary", dobinary, 0, 0, NULLCHAR, "cd", doftpcd, 0, 2, "cd ", "dir", dolist, 0, 0, NULLCHAR, "list", dolist, 0, 0, NULLCHAR, "get", doget, 0, 2, "get ", "hash", dohash, 0, 0, NULLCHAR, "ls", dols, 0, 0, NULLCHAR, "mget", domget, 0, 2, "mget [ ...]", "mkdir", domkdir, 0, 2, "mkdir ", "mput", domput, 0, 2, "mput [ ...]", "nlst", dols, 0, 0, NULLCHAR, "quit", doquit, 0, 0, NULLCHAR, "rmdir", dormdir, 0, 2, "rmdir ", "put", doput, 0, 2, "put ", "type", dotype, 0, 0, NULLCHAR, "verbose", doverbose, 0, 0, NULLCHAR, NULLCHAR, NULLFP, 0, 0, NULLCHAR, }; /* Handle top-level FTP command */ int doftp(argc,argv,p) int argc; char *argv[]; void *p; { struct session *sp; struct ftpcli ftp; struct sockaddr_in fsocket; int resp,vsave; char *buf,*bufsav,*cp; int control; /* Allocate a session control block */ if((sp = newsession(argv[1],FTP,1)) == NULLSESSION){ tprintf("Too many sessions\n"); return 1; } memset((char *)&ftp,0,sizeof(ftp)); ftp.control = ftp.data = -1; ftp.verbose = V_NORMAL; sp->cb.ftp = &ftp; /* Downward link */ ftp.session = sp; /* Upward link */ fsocket.sin_family = AF_INET; if(argc < 3) fsocket.sin_port = IPPORT_FTP; else fsocket.sin_port = atoi(argv[2]); tprintf("Resolving %s... ",sp->name); if((fsocket.sin_addr.s_addr = resolve(sp->name)) == 0){ tprintf(Badhost,sp->name); keywait(NULLCHAR,1); freesession(sp); return 1; } /* Open the control connection */ if((control = sp->s = ftp.control = socket(AF_INET,SOCK_STREAM,0)) == -1){ tprintf("Can't create socket\n"); keywait(NULLCHAR,1); freesession(sp); return 1; } settos(sp->s,LOW_DELAY); sockmode(sp->s,SOCK_ASCII); setflush(sp->s,-1); /* Flush output only when we call getresp() */ tprintf("Trying %s...\n",psocket((struct sockaddr *)&fsocket)); if(connect(control,(char *)&fsocket,sizeof(fsocket)) == -1) goto quit; tprintf("FTP session %u connected to %s\n",(unsigned)(sp-Sessions), sp->name); /* Wait for greeting from server */ resp = getresp(&ftp,200); if(resp >= 400) goto quit; /* Now process responses and commands */ buf = mallocw(LINELEN); while(resp != -1){ if(resp == 220){ /* Sign-on banner; prompt for and send USER command */ getline(sp,"Enter user name: ",buf,LINELEN); /* Send the command only if the user response * was non-null */ if(buf[0] != '\n'){ usprintf(control,"USER %s",buf); resp = getresp(&ftp,200); } else resp = 200; /* dummy */ } else if(resp == 331){ /* turn off echo */ sp->ttystate.echo = 0; getline(sp,"Password: ",buf,LINELEN); tprintf("\n"); /* Turn echo back on */ sp->ttystate.echo = 1; /* Send the command only if the user response * was non-null */ if(buf[0] != '\n'){ usprintf(control,"PASS %s",buf); resp = getresp(&ftp,200); } else resp = 200; /* dummy */ } else { /* Test the control channel first */ if(sockstate(control) == NULLCHAR) break; getline(sp,"ftp> ",buf,LINELEN); /* Copy because cmdparse modifies the original */ bufsav = strdup(buf); if((resp = cmdparse(Ftpcmds,buf,&ftp)) != -1){ /* Valid command, free buffer and get another */ free(bufsav); } else { /* Not a local cmd, send to remote server */ usputs(control,bufsav); free(bufsav); /* Enable display of server response */ vsave = ftp.verbose; ftp.verbose = V_NORMAL; resp = getresp(&ftp,200); ftp.verbose = vsave; } } } free(buf); quit: cp = sockerr(control); tprintf("FTP session %u closed: %s\n",(unsigned)(sp - Sessions), cp != NULLCHAR ? cp : "EOF"); if(ftp.fp != NULLFILE && ftp.fp != stdout) fclose(ftp.fp); if(ftp.data != -1) close_s(ftp.data); if(ftp.control != -1) close_s(ftp.control); keywait(NULLCHAR,1); if(ftp.session != NULLSESSION) freesession(ftp.session); return 0; } /* Control verbosity level */ static int doverbose(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; if((ftp = (struct ftpcli *)p) == NULLFTP) return -1; return setshort(&ftp->verbose,"Verbose",argc,argv); } /* Enable/disable command batching */ static int dobatch(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; if((ftp = (struct ftpcli *)p) == NULLFTP) return -1; return setbool(&ftp->batch,"Command batching",argc,argv); } /* Set verbosity to high (convenience command) */ static int dohash(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; if((ftp = (struct ftpcli *)p) == NULLFTP) return -1; ftp->verbose = V_HASH; return 0; } /* Close session */ static int doquit(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP) return -1; usprintf(ftp->control,"QUIT\n"); getresp(ftp,200); /* Get the closing message */ getresp(ftp,200); /* Wait for the server to close */ return -1; } /* Translate 'cd' to 'cwd' for convenience */ static int doftpcd(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP) return -1; usprintf(ftp->control,"CWD %s\n",argv[1]); return getresp(ftp,200); } /* Translate 'mkdir' to 'xmkd' for convenience */ static int domkdir(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP) return -1; usprintf(ftp->control,"XMKD %s\n",argv[1]); return getresp(ftp,200); } /* Translate 'rmdir' to 'xrmd' for convenience */ static int dormdir(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP) return -1; usprintf(ftp->control,"XRMD %s\n",argv[1]); return getresp(ftp,200); } static int dobinary(argc,argv,p) int argc; char *argv[]; void *p; { char *args[2]; args[1] = "I"; return dotype(2,args,p); } static int doascii(argc,argv,p) int argc; char *argv[]; void *p; { char *args[2]; args[1] = "A"; return dotype(2,args,p); } /* Handle "type" command from user */ static int dotype(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP) return -1; if(argc < 2){ switch(ftp->type){ case IMAGE_TYPE: tprintf("Image\n"); break; case ASCII_TYPE: tprintf("Ascii\n"); break; case LOGICAL_TYPE: tprintf("Logical bytesize %u\n",ftp->logbsize); break; } return 0; } switch(*argv[1]){ case 'i': case 'I': case 'b': case 'B': ftp->type = IMAGE_TYPE; break; case 'a': case 'A': ftp->type = ASCII_TYPE; break; case 'L': case 'l': ftp->type = LOGICAL_TYPE; ftp->logbsize = atoi(argv[2]); break; default: tprintf("Invalid type %s\n",argv[1]); return 1; } return 0; } /* Start receive transfer. Syntax: get [] */ static int doget(argc,argv,p) int argc; char *argv[]; void *p; { char *remotename,*localname; register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP){ tprintf(Notsess); return 1; } remotename = argv[1]; if(argc < 3) localname = remotename; else localname = argv[2]; getsub(ftp,"RETR",remotename,localname); return 0; } /* Get a collection of files */ static int domget(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; FILE *files; char tmpname[L_tmpnam+1]; char *buf; int i; long r; if((ftp = (struct ftpcli *)p) == NULLFTP){ tprintf(Notsess); return 1; } tmpnam(tmpname); buf = mallocw(DIRBUF); ftp->state = RECEIVING_STATE; for(i=1;iabort) break; /* Aborted */ if(r == -1 || (files = fopen(tmpname,"r")) == NULLFILE){ tprintf("Can't NLST %s\n",argv[i]); unlink(tmpname); continue; } /* The tmp file now contains a list of the remote files, so * go get 'em. Break out if the user signals an abort. */ while(fgets(buf,DIRBUF,files) != NULLCHAR){ rip(buf); getsub(ftp,"RETR",buf,buf); if(ftp->abort){ /* User abort */ ftp->abort = 0; fclose(files); unlink(tmpname); free(buf); ftp->state = COMMAND_STATE; return 1; } } fclose(files); unlink(tmpname); } free(buf); ftp->state = COMMAND_STATE; ftp->abort = 0; return 0; } /* List remote directory. Syntax: dir [] */ static int dolist(argc,argv,p) int argc; char *argv[]; void *p; { char *remotename,*localname; register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP){ tprintf(Notsess); return 1; } remotename = argv[1]; if(argc > 2) localname = argv[2]; else localname = NULLCHAR; getsub(ftp,"LIST",remotename,localname); return 0; } /* Remote directory list, short form. Syntax: ls [] */ static int dols(argc,argv,p) int argc; char *argv[]; void *p; { char *remotename,*localname; register struct ftpcli *ftp; ftp = (struct ftpcli *)p; if(ftp == NULLFTP){ tprintf(Notsess); return 1; } remotename = argv[1]; if(argc > 2) localname = argv[2]; else localname = NULLCHAR; getsub(ftp,"NLST",remotename,localname); return 0; } /* Common code to LIST/NLST/RETR and mget * Returns number of bytes received if successful * Returns -1 on error */ static long getsub(ftp,command,remotename,localname) register struct ftpcli *ftp; char *command,*remotename,*localname; { unsigned long total; FILE *fp; int cnt,resp,i,control,savmode; char *mode; struct sockaddr_in lsocket; struct sockaddr_in lcsocket; int32 startclk,rate; int vsave; int typewait = 0; int prevstate; if(ftp == NULLFTP) return -1; control = ftp->control; savmode = ftp->type; switch(ftp->type){ case IMAGE_TYPE: case LOGICAL_TYPE: mode = WRITE_BINARY; break; case ASCII_TYPE: mode = WRITE_TEXT; break; } /* Open the file */ if(localname == NULLCHAR){ fp = NULLFILE; } else if((fp = fopen(localname,mode)) == NULLFILE){ tprintf("Can't write %s: %s\n",localname,sys_errlist[errno]); return -1; } /* Open the data connection */ ftp->data = socket(AF_INET,SOCK_STREAM,0); listen(ftp->data,0); /* Accept only one connection */ prevstate = ftp->state; ftp->state = RECEIVING_STATE; /* Send TYPE message, if necessary */ if(strcmp(command,"LIST") == 0 || strcmp(command,"NLST") == 0){ /* Directory listings are always in ASCII */ ftp->type = ASCII_TYPE; } if(ftp->typesent != ftp->type){ switch(ftp->type){ case ASCII_TYPE: usprintf(control,"TYPE A\n"); break; case IMAGE_TYPE: usprintf(control,"TYPE I\n"); break; case LOGICAL_TYPE: usprintf(control,"TYPE L %d\n",ftp->logbsize); break; } ftp->typesent = ftp->type; if(!ftp->batch){ resp = getresp(ftp,200); if(resp == -1 || resp > 299) goto failure; } else typewait = 1; } /* Send the PORT message. Use the IP address * on the local end of our control connection. */ i = SOCKSIZE; getsockname(ftp->data,(char *)&lsocket,&i); /* Get port number */ i = SOCKSIZE; getsockname(ftp->control,(char *)&lcsocket,&i); lsocket.sin_addr.s_addr = lcsocket.sin_addr.s_addr; sendport(control,&lsocket); if(!ftp->batch){ /* Get response to PORT command */ resp = getresp(ftp,200); if(resp == -1 || resp > 299) goto failure; } /* Generate the command to start the transfer */ if(remotename != NULLCHAR) usprintf(control,"%s %s\n",command,remotename); else usprintf(control,"%s\n",command); if(ftp->batch){ /* Get response to TYPE command, if sent */ if(typewait){ resp = getresp(ftp,200); if(resp == -1 || resp > 299) goto failure; } /* Get response to PORT command */ resp = getresp(ftp,200); if(resp == -1 || resp > 299) goto failure; } /* Get the intermediate "150" response */ resp = getresp(ftp,100); if(resp == -1 || resp >= 400) goto failure; /* Wait for the server to open the data connection */ cnt = 0; ftp->data = accept(ftp->data,NULLCHAR,&cnt); startclk = msclock(); /* If output is to the screen, temporarily disable hash marking */ vsave = ftp->verbose; if(vsave >= V_HASH && fp == NULLFILE) ftp->verbose = V_NORMAL; total = recvfile(fp,ftp->data,ftp->type,ftp->verbose >= V_HASH); /* Immediately close the data connection; some servers (e.g., TOPS-10) * wait for the data connection to close completely before returning * the completion message on the control channel */ close_s(ftp->data); ftp->data = -1; #ifdef CPM if(fp != NULLFILE && ftp->type == ASCII_TYPE) putc(CTLZ,fp); #endif if(fp != NULLFILE && fp != stdout) fclose(fp); if(remotename == NULLCHAR) remotename = ""; if(total == -1){ tprintf("%s %s: Error/abort during data transfer\n",command,remotename); } else if(ftp->verbose >= V_SHORT){ startclk = msclock() - startclk; rate = 0; if(startclk != 0){ /* Avoid divide-by-zero */ if(total < 4294967L) { rate = (total*1000)/startclk; } else { /* Avoid overflow */ rate = total/(startclk/1000); } } tprintf("%s %s: %lu bytes in %lu sec (%lu/sec)\n", command,remotename, total,startclk/1000,rate); } /* Get the "Sent" message */ getresp(ftp,200); ftp->state = prevstate; ftp->verbose = vsave; ftp->type = savmode; return total; failure: /* Error, quit */ if(fp != NULLFILE && fp != stdout) fclose(fp); close_s(ftp->data); ftp->data = -1; ftp->state = prevstate; ftp->type = savmode; return -1; } /* Send a file. Syntax: put [] */ static int doput(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; char *remotename,*localname; if((ftp = (struct ftpcli *)p) == NULLFTP){ tprintf(Notsess); return 1; } localname = argv[1]; if(argc < 3) remotename = localname; else remotename = argv[2]; putsub(ftp,remotename,localname); return 0; } /* Put a collection of files */ static int domput(argc,argv,p) int argc; char *argv[]; void *p; { register struct ftpcli *ftp; FILE *files; int i; char tmpname[L_tmpnam+1]; char *buf; if((ftp = (struct ftpcli *)p) == NULLFTP){ tprintf(Notsess); return 1; } tmpnam(tmpname); if((files = fopen(tmpname,"w+")) == NULLFILE){ tprintf("Can't list local files\n"); unlink(tmpname); return 1; } for(i=1;istate = SENDING_STATE; while(fgets(buf,DIRBUF,files) != NULLCHAR){ rip(buf); putsub(ftp,buf,buf); if(ftp->abort) break; /* User abort */ } fclose(files); unlink(tmpname); free(buf); ftp->state = COMMAND_STATE; ftp->abort = 0; return 0; } /* Common code to put, mput. * Returns number of bytes sent if successful * Returns -1 on error */ static long putsub(ftp,remotename,localname) register struct ftpcli *ftp; char *remotename,*localname; { char *mode; int i,resp,control; unsigned long total; FILE *fp; struct sockaddr_in lsocket,lcsocket; int32 startclk,rate; int typewait = 0; int prevstate; control = ftp->control; if(ftp->type == IMAGE_TYPE) mode = READ_BINARY; else mode = READ_TEXT; /* Open the file */ if((fp = fopen(localname,mode)) == NULLFILE){ tprintf("Can't read %s: %s\n",localname,sys_errlist[errno]); return -1; } if(ftp->type == ASCII_TYPE && isbinary(fp)){ tprintf("Warning: type is ASCII and %s appears to be binary\n",localname); } /* Open the data connection */ ftp->data = socket(AF_INET,SOCK_STREAM,0); listen(ftp->data,0); prevstate = ftp->state; ftp->state = SENDING_STATE; /* Send TYPE message, if necessary */ if(ftp->typesent != ftp->type){ switch(ftp->type){ case ASCII_TYPE: usprintf(control,"TYPE A\n"); break; case IMAGE_TYPE: usprintf(control,"TYPE I\n"); break; case LOGICAL_TYPE: usprintf(control,"TYPE L %d\n",ftp->logbsize); break; } ftp->typesent = ftp->type; /* Get response to TYPE command */ if(!ftp->batch){ resp = getresp(ftp,200); if(resp == -1 || resp > 299){ goto failure; } } else typewait = 1; } /* Send the PORT message. Use the IP address * on the local end of our control connection. */ i = SOCKSIZE; getsockname(ftp->data,(char *)&lsocket,&i); i = SOCKSIZE; getsockname(ftp->control,(char *)&lcsocket,&i); lsocket.sin_addr.s_addr = lcsocket.sin_addr.s_addr; sendport(control,&lsocket); if(!ftp->batch){ /* Get response to PORT command */ resp = getresp(ftp,200); if(resp == -1 || resp > 299){ goto failure; } } /* Generate the command to start the transfer */ usprintf(control,"STOR %s\n",remotename); if(ftp->batch){ /* Get response to TYPE command, if sent */ if(typewait){ resp = getresp(ftp,200); if(resp == -1 || resp > 299){ goto failure; } } /* Get response to PORT command */ resp = getresp(ftp,200); if(resp == -1 || resp > 299){ goto failure; } } /* Get the intermediate "150" response */ resp = getresp(ftp,100); if(resp == -1 || resp >= 400){ goto failure; } /* Wait for the data connection to open. Otherwise the first * block of data would go out with the SYN, and this may confuse * some other TCPs */ accept(ftp->data,NULLCHAR,(int *)NULL); startclk = msclock(); total = sendfile(fp,ftp->data,ftp->type,ftp->verbose >= V_HASH); close_s(ftp->data); ftp->data = -1; fclose(fp); /* Wait for control channel ack before calculating transfer time; * this accounts for transmitted data in the pipe */ getresp(ftp,200); if(total == -1){ tprintf("STOR %s: Error/abort during data transfer\n",remotename); } else if(ftp->verbose >= V_SHORT){ startclk = msclock() - startclk; rate = 0; if(startclk != 0){ /* Avoid divide-by-zero */ if(total < 4294967L) { rate = (total*1000)/startclk; } else { /* Avoid overflow */ rate = total/(startclk/1000); } } tprintf("STOR %s: %lu bytes in %lu sec (%lu/sec)\n", remotename,total,startclk/1000,rate); } ftp->state = prevstate; return total; failure: /* Error, quit */ fclose(fp); close_s(ftp->data); ftp->data = -1; ftp->state = prevstate; return -1; } /* Abort a GET or PUT operation in progress. Note: this will leave * the partial file on the local or remote system */ int doabort(argc,argv,p) int argc; char *argv[]; void *p; { register struct session *sp; register struct ftpcli *ftp; sp = (struct session *)p; if(sp == NULLSESSION) return -1; /* Default is the current session, but it can be overridden with * an argument. */ if(argc > 1) sp = sessptr(argv[1]); if(sp == NULLSESSION || sp->type != FTP){ tprintf("Not an active FTP session\n"); return 1; } ftp = sp->cb.ftp; switch(ftp->state){ case COMMAND_STATE: tprintf("No active transfer\n"); return 1; case SENDING_STATE: /* Send a premature EOF. * Unfortunately we can't just reset the connection * since the remote side might end up waiting forever * for us to send something. */ shutdown(ftp->data,1); /* Note fall-thru */ ftp->abort = 1; break; case RECEIVING_STATE: /* Just blow away the receive socket */ shutdown(ftp->data,2); /* Note fall-thru */ ftp->abort = 1; break; } return 0; } /* send PORT message */ static void sendport(s,socket) int s; struct sockaddr_in *socket; { /* Send PORT a,a,a,a,p,p message */ usprintf(s,"PORT %u,%u,%u,%u,%u,%u\n", hibyte(hiword(socket->sin_addr.s_addr)), lobyte(hiword(socket->sin_addr.s_addr)), hibyte(loword(socket->sin_addr.s_addr)), lobyte(loword(socket->sin_addr.s_addr)), hibyte(socket->sin_port), lobyte(socket->sin_port)); } /* Wait for, read and display response from FTP server. Return the result code. */ static int getresp(ftp,mincode) struct ftpcli *ftp; int mincode; /* Keep reading until at least this code comes back */ { register char *line; int rval; usflush(ftp->control); line = mallocw(LINELEN); for(;;){ /* Get line */ if(recvline(ftp->control,line,LINELEN) == -1){ rval = -1; break; } rip(line); /* Remove cr/lf */ rval = atoi(line); if(rval >= 400 || ftp->verbose >= V_NORMAL) tprintf("%s\n",line); /* Display to user */ /* Messages with dashes are continued */ if(line[3] != '-' && rval >= mincode) break; } free(line); return rval; } /* Issue a prompt and read a line from the user */ static int getline(sp,prompt,buf,n) struct session *sp; char *prompt; char *buf; int n; { /* If there's something already there, don't issue prompt */ if(socklen(sp->input,0) == 0) tprintf(prompt); usflush(sp->output); return recvline(sp->input,buf,n); }