/* Open.c */

#include "Sys.h"

#ifdef SYSLOG
#	include <syslog.h>
#endif

#include "Open.h"
#include "Util.h"
#include "GetPass.h"
#include "Cmds.h"
#include "RCmd.h"
#include "Recent.h"
#include "FTP.h"
#include "Get.h"
#include "Getopt.h"
#include "Macro.h"
#include "Hostwin.h"
#include "Main.h"

/* This is a preference setting specifying whether we use anonymous logins
 * by default (as opposed to user/passwd logins).
 */
int gAnonOpen = (UOPEN == 0);

/* A structure containing some extra information we've learned about
 * the remote host, so we don't need to find out the hard way each
 * time we do something.
 */
extern RemoteSiteInfo gRmtInfo;

/* Name of the host we're connected to. */
string gHost;

/* We keep track if we've logged in to the host yet.  It's possible to
 * be connected but not authorized (authenticated, whatever) to do anything.
 */
int gLoggedIn = 0;

/* If we connect successfully, we'll want to save this port number
 * when we add a host file entry.
 */
int gPortNumberUsed;

int gSavePasswords = 0;

/* Open.c externs */
extern char *gOptArg;
extern int gOptInd, gIsToTTY;
extern unsigned int gFTPPort;
extern long gEventNumber;
extern char gRemoteCWD[256];
extern char gIPStr[32];
extern int gTransferType, gCurType;
extern int gRmtInfoIsNew, gWantRmtInfoSaved;
extern int gConnected, gHasPASV, gVerbosity;
extern int gDoneApplication, gWinInit;
extern string gEmailAddress, gAnonPassword;
extern UserInfo gUserInfo;
extern FILE *gLogFile;
extern LineList gRedir;


/* This is used primarily for non-anonymous logins.  We'll have to ask
 * the user some questions, like their username, password, etc.
 */
int LoginQuestion(
	char *prompt,
	char *answer,
	size_t siz,
	char *defanswer,
	int noEcho)
{
	string prompt2;
	
	/* Only do this if we have an empty string as the answer. */
	if (*answer == '\0') {
		if (defanswer != NULL)
			sprintf(prompt2, "%s [%s]: ", prompt, defanswer);
		else
			sprintf(prompt2, "%s: ", prompt);
		GetAnswer(prompt2, answer, siz, noEcho);
		if ((*answer == '\0') && (defanswer != NULL))
			Strncpy(answer, defanswer, siz);
	}
	return (*answer == '\0' ? (-1) : 0);
}	/* LoginQuestion */




int Login(char *u, char *p, char *a)
{
	string u2, p2, a2;
	ResponsePtr rp;
	int result = -1;
	int isAnonLogin = 1;

	STRNCPY(u2, u);
	STRNCPY(p2, p);
	STRNCPY(a2, a);
		
	rp = InitResponse();
	if (LoginQuestion("User", u2, sizeof(u2), "anonymous", 0) < 0)
		goto done;
	RCmd(rp, "USER %s", u2);

	for (;;) {
		/* Here's a mini finite-automaton for the login process.
		 *
		 * Originally, the FTP protocol was designed to be entirely
		 * implementable from a FA.  It could be done, but I don't think
		 * it's something an interactive process could be the most
		 * effective with.
		 */
		switch (rp->code) {
			case 230:	/* 230 User logged in, proceed. */
			case 202:	/* Command not implemented, superfluous at this site. */
				goto okay;

			case 421:	/* 421 Service not available, closing control connection. */
				break;
				
			case 331:	/* 331 User name okay, need password. */
				ReInitResponse(rp);
				isAnonLogin =	STREQ("anonymous", u2) ||
								STREQ("ftp", u2);
				(void) LoginQuestion(
					"Password",
					p2,
					sizeof(p2),
					isAnonLogin ? gAnonPassword : NULL,
					1
				);
				RCmd(rp, "PASS %s", p2);
				break;

         	case 332:	/* 332 Need account for login. */
         	case 532: 	/* 532 Need account for storing files. */
				ReInitResponse(rp);
				(void) LoginQuestion("Account", a2, sizeof(a2), NULL, 1);
				RCmd(rp, "ACCT %s", a2);
				break;

			case 501:	/* Syntax error in parameters or arguments. */
			case 503:	/* Bad sequence of commands. */
			case 530:	/* Not logged in. */
			case 550:	/* Can't set guest privileges. */
				goto done;
				
			default:
				Error(kDontPerror, "Unexpected response: %s\n", rp->msg.first->line);
				goto done;
		}
	}
okay:
	if (isAnonLogin) {
		gRmtInfo.user[0] = '\0';
		gRmtInfo.pass[0] = '\0';
	} else {
		STRNCPY(gRmtInfo.user, u2);
		if (gSavePasswords)
			STRNCPY(gRmtInfo.pass, p2);
	}
	STRNCPY(gRmtInfo.acct, a2);

	result = 0;
done:
	DoneWithResponse(rp);
	return result;
}	/* Login */




/* After closing a site, set or restore some things to their original
 * states.
 */
void PostCloseStuff(void)
{
#ifdef SYSLOG
	syslog (LOG_INFO, "%s disconnected from %s.", gUserInfo.userName, gHost);
#endif
	if (gLoggedIn) {
		gRmtInfo.hasPASV = gHasPASV;
		gRmtInfo.port = gPortNumberUsed;
		gRmtInfo.nCalls++;
		time((time_t *) &gRmtInfo.lastCall);
		STRNCPY(gRmtInfo.lastIP, gIPStr);
		if (gEventNumber > 0L) {
			/* Only do these if we were not in batch mode (colon-mode). */
			STRNCPY(gRmtInfo.dir, gRemoteCWD);
			(void) RunPrefixedMacro("close.", gRmtInfo.nickName);
			(void) RunPrefixedMacro("close.", "any");
		} else {
			/* Only do these if we are running colon mode. */
			(void) RunPrefixedMacro("colon.close.", gRmtInfo.nickName);
			(void) RunPrefixedMacro("colon.close.", "any");
		}
		SaveRemoteInfo(NULL);
	}
	gLoggedIn = 0;
	gRemoteCWD[0] = gHost[0] = '\0';
	if (gDoneApplication == 0)	
		SetScreenInfo();
}	/* PostCloseStuff */




/* Close the connection to the remote host and cleanup. */
void DoClose(void)
{
	ResponsePtr rp;
	
	if (gConnected) {
		rp = InitResponse();
		rp->eofOkay = 1;	/* We are expecting EOF after this cmd. */
		RCmd(rp, "QUIT");
		DoneWithResponse(rp);
	}
	
	CloseControlConnection();
	PostCloseStuff();
}	/* DoClose */




int CloseCmd(int argc, char **argv)
{
	DoClose();
	return kNoErr;
}	/* CloseCmd */




/* Given a pointer to an OpenOptions (structure containing all variables
 * that can be set from the command line), this routine makes sure all
 * the variables have valid values by setting them to their defaults.
 */
 
void InitOpenOptions(OpenOptions *openopt)
{
	/* How do you want to open a site if neither -a or -u are given?
	 * gAnonOpen is true (default to anonymous login), unless
	 * defaults.h was edited to set UOPEN to 0 instead.
	 */
	openopt->openmode = gAnonOpen ? kOpenImplicitAnon : kOpenImplicitUser;

	/* Normally you don't want to ignore the entry in your netrc. */
	openopt->ignore_rc = 0;

	/* Set the default delay if the user specifies redial mode without
	 * specifying the redial delay.
	 */
	openopt->redial_delay = kRedialDelay;

	/* Normally, you only want to try once. If you specify redial mode,
	 * this is changed.
	 */
	openopt->max_dials = 1;
	
	/* You don't want to dump the file to stdout by default. */
	openopt->ftpcat = kNoFTPCat;

	/* We'll set this later, but we'll use 0 to mean that the port
	 * is explicitly not set by the user.
	 */
	openopt->port = kPortUnset;

	/* We are not in colon-mode (yet). */
	openopt->colonmodepath[0] = 0;

	/* Set the hostname to a null string, since there is no default host. */
	openopt->hostname[0] = 0;
	
	/* Set the opening directory path to a null string. */
	openopt->cdpath[0] = 0;

	/* Since we're opening with a possible colon-mode item, we have to
	 * track options for the GetCmd too.
	 */
	InitGetOptions(&openopt->gopt);
}	/* InitOpenOptions */





/* This is responsible for parsing the command line and setting variables
 * in the OpenOptions structure according to the user's flags.
 */

int GetOpenOptions(int argc, char **argv, OpenOptions *openopt, int fromMain)
{
	int					opt, www;
	char				*cp, *hostp, *cpath;

	/* First setup the openopt variables. */
	InitOpenOptions(openopt);

	/* Tell Getopt() that we want to start over with a new command. */
	GetoptReset();
	while ((opt = Getopt(argc, argv, "aiup:rd:g:cmfGRn:")) >= 0) {
		switch (opt) {		
			case 'a':
				/* User wants to open anonymously. */
				openopt->openmode = kOpenExplicitAnon;
				break;
				
			case 'u':
				/* User wants to open with a login and password. */
				openopt->openmode = kOpenExplicitUser;
				break;

			case 'i':
				/* User wants to ignore the entry in the netrc. */
				openopt->ignore_rc = 1;
				break;

			case 'p':
				/* User supplied a port number different from the default
				 * ftp port.
				 */
				openopt->port = atoi(gOptArg);
				if (openopt->port <= 0) {
					/* Probably never happen, but just in case. */
					(void) PrintF("%s: bad port number (%s).\n", argv[0],
						gOptArg);
					goto usage;
				}
				break;
				
			case 'd':
				/* User supplied a delay (in seconds) that differs from
				 * the default.
				 */
				openopt->redial_delay = atoi(gOptArg);
				break;
				
			case 'g':
				/* User supplied an upper-bound on the number of redials
				 * to try.
				 */
				openopt->max_dials = atoi(gOptArg);
				break;

			case 'r':
				openopt->max_dials = -1;
				break;

			case 'm':
				/* ftpcat mode is only available from your shell command-line,
				 * not from the ncftp shell.  Do that yourself with 'more zz'.
				 */
				if (gEventNumber == 0L) {
					/* If gEventNumber is zero, then we were called directly
					 * from main(), and before the ftp shell has started.
					 */
					openopt->ftpcat = kFTPMore;
					/* ftpcat mode is really ftpmore mode. */
					break;
				} else {
					PrintF(
"You can only use this form of colon-mode (-m) from your shell command line.\n\
Try 'ncftp -m wuarchive.wustl.edu:/README'\n");
				}
				goto usage;

			case 'c':
				/* ftpcat mode is only available from your shell command-line,
				 * not from the ncftp shell.  Do that yourself with 'get zz -'.
				 */
				if (gEventNumber == 0L) {
					/* If gEventNumber is zero, then we were called directly
					 * from main(), and before the ftp shell has started.
					 */
					openopt->ftpcat = kFTPCat;
					break;
				} else {
					PrintF(
"You can only use ftpcat/colon-mode from your shell command line.\n\
Try 'ncftp -c wuarchive.wustl.edu:/README > file.'\n");
				}
				goto usage;

			/* These are options that can be passed to get. */
			case 'f':
			case 'G':
			case 'R':
			/* case 'z':  Note that we can't handle the rename here. */
			case 'n':
				if (fromMain) {
					(void) SetGetOption(&openopt->gopt, opt, gOptArg);
					break;
				}
				goto usage;

			default:
				if (fromMain)
					break;
					
			usage:
				return kUsageErr;
		}
	}

	if (argv[gOptInd] == NULL) {
		if (!fromMain) {
			if (openopt->hostname[0] == 0)
				goto usage;
		}
	} else {
		/* The user gave us a host to open.
		 *
		 * First, check to see if they gave us a colon-mode path
		 * along with the hostname.  We also understand a WWW path,
		 * like "ftp://bang.nta.no/pub/fm2html.v.0.8.4.tar.Z".
		 */
		hostp = argv[gOptInd];
		cpath = NULL;
		if ((cp = strchr(hostp, ':')) != NULL) {
			if (fromMain) {
				*cp++ = '\0';
				cpath = cp;
				www = 0;	/* Is 0 or 1, depending on the type of path. */
				if ((*cp == '/') && (cp[1] == '/')) {
					/* First make sure the path was intended to be used
					 * with ftp and not one of the other URLs.
					 */
					if (!ISTREQ(argv[gOptInd], "ftp")) {
						PrintF(
							"Bad URL '%s' -- WWW paths must be prefixed by 'ftp://'.\n",
							argv[gOptInd]
						);
						goto usage;
					}
	
					cp += 2;
					hostp = cp;
					cpath = NULL;	/* It could have been ftp://hostname only. */
	
					if ((cp = strchr(hostp, '/')) != NULL) {
						*cp++ = '\0';
						cpath = cp;
					}
					www = 1;
				}
				if (cpath != NULL) {
					(void) STRNCPY(openopt->colonmodepath, www ? "/" : "");
					(void) STRNCAT(openopt->colonmodepath, cpath);
					DebugMsg("Colon-Mode Path = '%s'\n", openopt->colonmodepath);
				}

			} else {
				/* I may lift this restriction later. */
				EPrintF("You can't use colon mode in the command shell.\n");
				return kUsageErr;
			}
		}
		(void) STRNCPY(openopt->hostname, hostp);

		if ((openopt->colonmodepath[0] == '\0') &&
			(openopt->ftpcat != kNoFTPCat))
		{
			/* User specified ftpcat mode, but didn't supply
			 * the host:file.
			 */
			EPrintF("You didn't use colon mode correctly.\n\
If you use -c or -m, you need to do something like this:\n\
ncftp -c wuarchive.wustl.edu:/pub/README (to cat this file to stdout).\n");
			return kUsageErr;
		}

		GetRemoteInfo(openopt->hostname, sizeof(openopt->hostname));
		if (openopt->port == kPortUnset) {
			/* No port specified, so use same port as last time. */
			if (gRmtInfo.port == kPortUnset)
				openopt->port = gFTPPort;
			else
				openopt->port = gRmtInfo.port;
		}
	}
	return kNoErr;
}	/* GetOpenOptions */




void CheckRemoteSystemType(int force_binary)
{
	string remoteSys;

	if (gRmtInfoIsNew) {
		/* If first time here, check somethings while we can. */
		if (DoSystem(remoteSys, sizeof(remoteSys)) == 0)
			gRmtInfo.isUnix = !strncmp(remoteSys, "UNIX", (size_t) 4);
	
		/* Set to binary mode if any of the following are true:
		 * (a) The user is using colon-mode (force_binary; below);
		 * (b) The reply-string from SYST said it was UNIX with 8-bit chars.
		 */
		if (!strncmp(remoteSys, "UNIX Type: L8", (size_t) 13))
		{
			gRmtInfo.xferType = 'I';
		}
	
		/* Print a warning for that (extremely) rare Tenex machine. */
		if (!strncmp(remoteSys, "TOPS20", (size_t) 6)) {
			gRmtInfo.xferType = 'T';
			(void) PrintF("Using tenex mode to transfer files.\n");
		}
	}
	gTransferType = gRmtInfo.xferType;
	
	/* Use binary mode if this site was last set using ascii mode,
	 * and we are using colon-mode.
	 */
	if ((force_binary) && (gTransferType == 'A'))
		gTransferType = 'I';
}	/* CheckRemoteSystemType */



/* This is called if the user opened the host with a file appended to
 * the host's name, like "wuarchive.wustl.edu:/pub/readme," or
 * "wuarchive.wustl.edu:/pub."  In the former case, we open wuarchive,
 * and fetch "readme."  In the latter case, we open wuarchive, then set
 * the current remote directory to "/pub."  If we are fetching a file,
 * we can do some other tricks if "ftpcat mode" is enabled.  This mode
 * must be selected from your shell's command line, and this allows you
 * to use the program as a one-liner to pipe a remote file into something,
 * like "ncftp -c wu:/pub/README | wc."  If the user uses ftpcat mode,
 * the program immediately quits instead of going into its own command
 * shell.
 */
void ColonMode(OpenOptions *openopt)
{
	int result;

	/* Check for FTP-cat mode, so we call the appropriate
	 * fetching routine.
	 */
	if (openopt->ftpcat == kFTPCat) {
		InitGetOutputMode(&openopt->gopt, kDumpToStdout);
		openopt->gopt.rName = openopt->colonmodepath;
		openopt->gopt.doReports = 0;
		result = DoGet(&openopt->gopt);
	} else if (openopt->ftpcat == kFTPMore) {
		result = DoPage(openopt->colonmodepath);
	} else {
		/* Regular colon-mode, where we fetch the file, putting the
		 * copy in the current local directory.
		 */
		InitGetOutputMode(&openopt->gopt, kSaveToDisk);
		openopt->gopt.doReports = 0;
		openopt->gopt.rName = openopt->colonmodepath;
		result = DoGetWithGlobbingAndRecursion(&openopt->gopt);
	}
	DoQuit(result == 0 ? result : kExitColonModeFail);
	/*NOTREACHED*/
}	/* ColonMode */




/* Now that we're logged in, do some other stuff prior to letting the
 * user type away at the shell.
 */
void PostLoginStuff(OpenOptions *openopt)
{
	time_t now;
	
	gLoggedIn = 1;

	/* Clear out the old redir buffer, since we are on a new site. */
	DisposeLineListContents(&gRedir);

	/* Since we're logged in okay, we know what we've collected so far
	 * should be saved for next time.
	 */
	gWantRmtInfoSaved = 1;
	
	/* The FTP module keeps its own note of whether the site has PASV
	 * or not, and has already initialized it to true.  If the gRmtInfo
	 * was fetched from our host file, we can tell FTP.c for sure if
	 * PASV should even be attempted.  If this was a new gRmtInfo, we
	 * will just be a little redundant, since new entries also assume
	 * PASV is supported.
	 */
	gHasPASV = gRmtInfo.hasPASV;
	
	/* Since we connected okay, save this port number for later. */
	gPortNumberUsed = openopt->port;

	/* When a new site is opened, ASCII mode is assumed (by protocol). */
	gCurType = 'A';
	
	STRNCPY(gHost, openopt->hostname);
#ifdef SYSLOG
	syslog (LOG_INFO, "%s logged into %s.", gUserInfo.userName, gHost);
#endif
	if (gLogFile != NULL) {
		(void) time(&now);
		fprintf(gLogFile, "%s at %s", gHost, ctime(&now));
	}

	/* We need to check for unix and see if we should set binary
	 * mode automatically.
	 */
	CheckRemoteSystemType(openopt->colonmodepath[0] != (char)0);

	if (openopt->colonmodepath[0]) {
		/* Freshen 'cwd' variable for the prompt.
		 * It should be the root directory.
		 */
		(void) GetRemoteCWD(kDidNotChdir);
		ColonMode(openopt);
	} else if (gRmtInfo.dir[0]) {
		/* If we didn't have a colon-mode path, we try setting
		 * the current remote directory to cdpath.  The .dir field is
		 * (usually) the last directory we were in the previous
		 * time we called this site.
		 */
		(void) DoChdir(gRmtInfo.dir);
	} else {
		/* Freshen 'cwd' variable for the prompt. */
		(void) GetRemoteCWD(kDidNotChdir);
	}
	
	/* This updates the prompt, and for visual mode, the status bar. */
	SetScreenInfo();

	if (gEventNumber == 0L) {
		/* Run separate sets of macros for colon-mode opens and regular,
		 * interactive opens.
		 */
		(void) RunPrefixedMacro("colon.open.", "any");
		(void) RunPrefixedMacro("colon.open.", gRmtInfo.nickName);
	} else {
		(void) RunPrefixedMacro("open.", "any");
		(void) RunPrefixedMacro("open.", gRmtInfo.nickName);
	}
}	/* PostLoginStuff */





/* Given a properly set up OpenOptions, we try connecting to the site,
 * redialing if necessary, and do some initialization steps so the user
 * can send commands.
 */
int Open(OpenOptions *openopt)
{
	int					hErr;
	int					dials;
	char				*user, *pass, *r_acct;	
	int					loginResult;
	int					openMode;

	if (ISEXPLICITOPEN(openopt->openmode)) {
		/* User specified an open mode explictly, like open -u,
		 * so do what the user said in spite of what we may have had
		 * in the gRmtInfo.
		 */
		openMode = (ISANONOPEN(openopt->openmode)) ? 'a' : 'u';
	} else {
		if (gRmtInfoIsNew == 1) {
			/* First time opening this site.  Open it anonymously
			 * unless you said not to with a -option.
			 */
			openMode = (ISANONOPEN(openopt->openmode)) ? 'a' : 'u';
		} else {
			/* We've been here before, so use what we had last time. */
			openMode = 'r';
		}
	}

	if ((openMode == 'r') && (gRmtInfo.user[0] == '\0'))
		openMode = 'a';
	if (openMode == 'a') {
		user = "anonymous";
		pass = strchr(gRmtInfo.pass, '@') ? gRmtInfo.pass : gAnonPassword;
		r_acct = "";
	} else if (openMode == 'u') {
		user = "";
		pass = "";
		r_acct = "";
	} else {
		user = gRmtInfo.user;
		pass = gRmtInfo.pass;
		r_acct = gRmtInfo.acct;
	}

	if (openopt->colonmodepath[0]) {
		/* If we're running from a shell script (or whatever)
		 * don't dump any output.  If the user is doing this from
		 * the shell, we will at least print the error messages.
		 * We also don't want to "pollute" ftpcat mode, but since
		 * error messages are printed to stderr, and we weren't
		 * going to print anything but error messages anyway,
		 * we're okay by using kErrorsOnly.
		 */
		if (gIsToTTY)
			(void) SetVerbose(kErrorsOnly);
		else
			(void) SetVerbose(kQuiet);
	}
	
	PrintF("Trying to connect to %s...\n", openopt->hostname);
	for (
			dials = 0;
			openopt->max_dials < 0 || dials < openopt->max_dials;
			dials++)
	{
		if (dials > 0) {
			/* If this is the second dial or higher, sleep a bit. */
			PrintF("Sleeping %u seconds...  ",
				(unsigned) openopt->redial_delay);
			FlushListWindow();
			(void) sleep((unsigned) openopt->redial_delay);
			PrintF("Retry Number: %d\n", dials + 1);
		}
		FlushListWindow();

		hErr = OpenControlConnection(openopt->hostname, openopt->port);
		if (hErr == kConnectErrFatal) {
			/* Irrecoverable error, so don't bother redialing.
			 * The error message should have already been printed
			 * from Hookup().
			 */
			DebugMsg("Cannot recover from open error %d.\n", hErr);
			break;
		} else if (hErr == kConnectNoErr) {
			/* We were hooked up successfully. */
			
			gRemoteCWD[0] = '\0';
	
			/* This updates the status bar (for visual mode). */
			SetScreenInfo();
			SetPostHangupOnServerProc(PostCloseStuff);
			loginResult = Login(user, pass, r_acct);
			
			if (loginResult == 0) {
				PostLoginStuff(openopt);
				if (openopt->max_dials != 1) {
					/* If they selected redial mode, beep at the user
					 * to get their attention.
					 */
					Beep(1);
				}
				return(kNoErr);	/* Login okay, so done. */
			}
			/* Otherwise, an error during login occurred, so try again. */
		} else /* (hErr == kConnectErrReTryable), so redial. */ {
			/* Display error now. */
			FlushListWindow();
		}
		
		DoClose();
	}

	if (openopt->colonmodepath[0]) {
		/* If we get here, we we're colon-moding and got non-redialable
		 * error or we ran out of attempts.
		 */
		DoQuit(kExitColonModeFail);
	}
	return (kCmdErr);
}	/* Open */




int OpenCmd(int argc, char **argv)
{
	OpenOptions			openopt;
	int result;

	/* If there is already a site open, close that one so we can
	 * open a new one.
	 */
	DoClose();

	if (argc < 2) {
		result = HostWindow();
	} else {
		if ((result = GetOpenOptions(argc, argv, &openopt, 0)) == kNoErr)
			result = Open(&openopt);
	}
	return result;
}	/* OpenCmd */

/* Open.c */
