// $Id: Login.cc,v 1.16 2003/08/31 00:38:11 vern Exp $
//
// Copyright (c) 1999, 2001, 2002
//      The Regents of the University of California.  All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that: (1) source code distributions
// retain the above copyright notice and this paragraph in its entirety, (2)
// distributions including binary code include the above copyright notice and
// this paragraph in its entirety in the documentation or other materials
// provided with the distribution, and (3) all advertising materials mentioning
// features or use of this software display the following acknowledgement:
// ``This product includes software developed by the University of California,
// Lawrence Berkeley Laboratory and its contributors.'' Neither the name of
// the University nor the names of its contributors may be used to endorse
// or promote products derived from this software without specific prior
// written permission.
// THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
// WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

#include "config.h"

#include <ctype.h>
#include <stdlib.h>

#include "NetVar.h"
#include "Login.h"
#include "RE.h"
#include "Event.h"

static RE_Matcher* re_skip_authentication = 0;
static RE_Matcher* re_direct_login_prompts;
static RE_Matcher* re_login_prompts;
static RE_Matcher* re_login_non_failure_msgs;
static RE_Matcher* re_login_failure_msgs;
static RE_Matcher* re_login_success_msgs;
static RE_Matcher* re_login_timeouts;

static RE_Matcher* init_RE(ListVal* l);

LoginConn::LoginConn(NetSessions* s, HashKey* k, double t, const ConnID* id,
		const struct tcphdr* tp)
: TCP_Connection(s, k, t, id, tp)
	{
	state = LOGIN_STATE_AUTHENTICATE;
	login_conn = this;
	num_user_lines_seen = lines_scanned = 0;
	// Set last_failure_num_user_lines so we will always generate
	// at least one failure message, even if the user doesn't
	// type anything (but we see, e.g., a timeout).
	last_failure_num_user_lines = -1;
	login_prompt_line = failure_line = 0;
	user_text_first = 0;
	user_text_last = MAX_USER_TEXT - 1;
	num_user_text = 0;
	client_name = username = 0;
	saw_ploy = is_VMS = 0;

	if ( ! re_skip_authentication )
		{
		re_skip_authentication = init_RE(skip_authentication);
		re_direct_login_prompts = init_RE(direct_login_prompts);
		re_login_prompts = init_RE(login_prompts);
		re_login_non_failure_msgs = init_RE(login_non_failure_msgs);
		re_login_failure_msgs = init_RE(login_failure_msgs);
		re_login_success_msgs = init_RE(login_success_msgs);
		re_login_timeouts = init_RE(login_timeouts);
		}
	}

LoginConn::~LoginConn()
	{
	while ( num_user_text > 0 )
		delete PopUserText();

	Unref(username);
	Unref(client_name);
	}

void LoginConn::NewLine(TCP_Endpoint* s, int length, const char* line, int /* is_const */)
	{
	char* str = new char[length+1];

	// Eliminate NUL characters.
	int i, j;
	for ( i = 0, j = 0; i < length; ++i )
		if ( line[i] != '\0' )
			str[j++] = line[i];
		else 
			{
			if ( FlagEvent(NUL_IN_LINE) )
				Weird("NUL_in_line");
			}

	str[j] = '\0';

	NewLine(s, str);
	delete str;
	}

void LoginConn::NewLine(TCP_Endpoint* s, char* line)
	{
	if ( state == LOGIN_STATE_SKIP )
		return;

	if ( s == orig && login_input_line )
		LineEvent(login_input_line, line);

	if ( s == resp && login_output_line )
		LineEvent(login_output_line, line);

	if ( state == LOGIN_STATE_LOGGED_IN )
		return;

	if ( state == LOGIN_STATE_AUTHENTICATE )
		{
		if ( orig->state == TCP_PARTIAL || resp->state == TCP_PARTIAL )
			state = LOGIN_STATE_CONFUSED;	// unknown login state
		else
			{
			AuthenticationDialog(s, line);
			return;
			}
		}

	if ( state != LOGIN_STATE_CONFUSED )
		internal_error("bad state in LoginConn::NewLine");

	// When we're in "confused", we feed each user input line to
	// login_confused_text, but also scan the text in the
	// other direction for evidence of successful login.
	if ( s == orig )
		{
		(void) IsPloy(line);
		ConfusionText(line);
		}

	else if ( ! saw_ploy && IsSuccessMsg(line) )
		{
		LoginEvent(login_success, line, 1);
		state = LOGIN_STATE_LOGGED_IN;
		}
	}

void LoginConn::AuthenticationDialog(TCP_Endpoint* s, char* line)
	{
	if ( s == orig )
		{
		if ( is_VMS )
			{
#define VMS_REPEAT_SEQ "\x1b[A"
			char* repeat_prev_line = strstr(line, VMS_REPEAT_SEQ);
			if ( repeat_prev_line )
				{
				if ( repeat_prev_line[strlen(VMS_REPEAT_SEQ)] )
					{
					Confused("extra_repeat_text", line);
					return;
					}

				// VMS repeats the username, not the last line
				// typed (which presumably is the password).
				if ( username )
					{
					line = (char*) username->AsString()->Bytes();
					if ( strstr(line, VMS_REPEAT_SEQ) )
						Confused("username_with_embedded_repeat", line);
					else
						NewLine(s, line);
					}

				else
					Confused("repeat_without_username", line);
				return;
				}
			}

		++num_user_lines_seen;

		if ( ! IsPloy(line) )
			AddUserText(line);

		return;
		}

	// Ignore blank lines from the responder - some systems spew
	// out a whole bunch of these.
	if ( IsEmpty(line) )
		return;

	if ( ++lines_scanned > MAX_AUTHENTICATE_LINES &&
	     login_prompt_line == 0 && failure_line == 0 )
		Confused("no_login_prompt", line);

	const char* prompt = IsLoginPrompt(line);
	int is_timeout = IsTimeout(line);
	if ( prompt && ! IsSuccessMsg(line) && ! is_timeout )
		{
		is_VMS = strstr(line, "Username:") != 0;

		// If we see multiple login prompts, presume that
		// each is consuming one line of typeahead.
		//
		// We can also get multiple login prompts spread
		// across adjacent lines, for example if the user
		// enters a blank line or a line that wasn't accepted
		// (e.g., "foo^C").

		int multi_line_prompt =
			(login_prompt_line == lines_scanned - 1 &&
			 // if login_prompt_line is the same as failure_line,
			 // then we didn't actually see a login prompt
			 // there, we're just remembering that as the
			 // prompt line so we can count typeahead with
			 // respect to it (see below).
			 login_prompt_line != failure_line);

		const char* next_prompt = 0;
		while ( (*prompt != '\0' &&
			 (next_prompt = IsLoginPrompt(prompt + 1))) ||
			multi_line_prompt )
			{
			if ( ! HaveTypeahead() )
				{
				Confused("multiple_login_prompts", line);
				break;
				}

			char* pop_str = PopUserText();

			if ( multi_line_prompt )
				break;

			if ( ! IsEmpty(pop_str) )
				{
				Confused("non_empty_multi_login", line);
				break;
				}

			prompt = next_prompt;
			}

		if ( state == LOGIN_STATE_CONFUSED )
			return;

		const char* user = GetUsername(prompt);
		if ( user && ! IsEmpty(user) )
			{
			if ( ! HaveTypeahead() )
				AddUserText(user);
			}

		login_prompt_line = lines_scanned;

		if ( IsDirectLoginPrompt(line) )
			{
			LoginEvent(login_success, line, 1);
			state = LOGIN_STATE_LOGGED_IN;
			SetSkip(1);
			return;
			}
		}

	else if ( is_timeout || IsFailureMsg(line) )
		{
		if ( num_user_lines_seen > last_failure_num_user_lines )
			{
			// The user has typed something since we last
			// generated a failure event, so it's worth
			// recording another failure event.
			//
			// We can otherwise wind up generating multiple
			// failure events for sequences like:
			//
			//	Error reading command input
			//	Timeout period expired
			if ( is_timeout )
				AddUserText("<timeout>");
			LoginEvent(login_failure, line);
			}

		// Set the login prompt line to be here, too, so
		// that we require MAX_LOGIN_LOOKAHEAD beyond this
		// point before deciding they've logged in.
		login_prompt_line = failure_line = lines_scanned;
		last_failure_num_user_lines = num_user_lines_seen;
		}

	else if ( IsSkipAuthentication(line) )
		{
		if ( authentication_skipped )
			{
			val_list* vl = new val_list;
			vl->append(BuildConnVal());
			ConnectionEvent(authentication_skipped, vl);
			}

		state = LOGIN_STATE_SKIP;
		SetSkip(1);
		}

	else if ( IsSuccessMsg(line) ||
		  (login_prompt_line > 0 &&
		   lines_scanned >
		   login_prompt_line + MAX_LOGIN_LOOKAHEAD + num_user_text) )
		{
		LoginEvent(login_success, line);
		state = LOGIN_STATE_LOGGED_IN;
		}
	}

void LoginConn::SetEnv(TCP_Endpoint* sender, char* name, char* val)
	{
	if ( sender == resp )
		// Why is the responder transmitting its environment??
		Confused("responder_environment", name);

	else
		{
		if ( streq(name, "USER") )
			{
			if ( username )
				{
				BroString* u = username->AsString();
				const byte_vec ub = u->Bytes();
				const char* us = (const char*) ub;
				if ( ! streq(val, us) )
					Confused("multiple_USERs", val);
				Unref(username);
				}

			// "val" gets copied here.
			username = new StringVal(val);
			}

		else if ( login_terminal && streq(name, "TERM") )
			{
			val_list* vl = new val_list;

			vl->append(BuildConnVal());
			vl->append(new StringVal(val));

			ConnectionEvent(login_terminal, vl);
			}

		else if ( login_display && streq(name, "DISPLAY") )
			{
			val_list* vl = new val_list;

			vl->append(BuildConnVal());
			vl->append(new StringVal(val));

			ConnectionEvent(login_display, vl);
			}

		else if ( login_prompt && streq(name, "TTYPROMPT") )
			{
			val_list* vl = new val_list;

			vl->append(BuildConnVal());
			vl->append(new StringVal(val));

			ConnectionEvent(login_prompt, vl);
			}
		}

	delete name;
	delete val;
	}

void LoginConn::EndpointEOF(TCP_EndpointContents* /* endp */)
	{
	if ( state == LOGIN_STATE_AUTHENTICATE && HaveTypeahead() )
		{
		LoginEvent(login_success, "<EOF>", 1);
		state = LOGIN_STATE_LOGGED_IN;
		}
	}

void LoginConn::LoginEvent(EventHandlerPtr f, const char* line, int no_user_okay)
	{
	if ( ! f )
		return;

	if ( login_prompt_line > failure_line )
		{
		FlushEmptyTypeahead();

		// We should've seen a username.
		if ( ! HaveTypeahead() )
			{
			if ( no_user_okay )
				{
				Unref(username);
				username = new StringVal("<none>");
				}

			else
				{
				Confused("no_username", line);
				return;
				}
			}
		else
			{
			Unref(username);
			username = PopUserTextVal();
			}
		}

	else
		{
		// Evidently the system reprompted for a password upon an
		// earlier failure.  Use the previously-recorded username.
		if ( ! username )
			{
			if ( no_user_okay )
				{
				Unref(username);
				username = new StringVal("<none>");
				}

			else
				{
				Confused("no_username2", line);
				return;
				}
			}
		}

	Val* password = HaveTypeahead() ?
				PopUserTextVal() : new StringVal("<none>");

	val_list* vl = new val_list;

	vl->append(BuildConnVal());
	vl->append(username->Ref());
	vl->append(client_name ? client_name->Ref() : new StringVal(""));
	vl->append(password);
	vl->append(new StringVal(line));

	ConnectionEvent(f, vl);
	}

const char* LoginConn::GetUsername(const char* line) const
	{
	while ( isspace(*line) )
		++line;

	return line;
	}

void LoginConn::LineEvent(EventHandlerPtr f, const char* line)
	{
	val_list* vl = new val_list;

	vl->append(BuildConnVal());
	vl->append(new StringVal(line));

	ConnectionEvent(f, vl);
	}


void LoginConn::Confused(const char* msg, const char* line)
	{
	state = LOGIN_STATE_CONFUSED;	// to suppress further messages

	if ( login_confused )
		{
		val_list* vl = new val_list;
		vl->append(BuildConnVal());
		vl->append(new StringVal(msg));
		vl->append(new StringVal(line));

		ConnectionEvent(login_confused, vl);
		}

	if ( login_confused_text )
		{
		// Send all of the typeahead, and the current line, as
		// confusion text.
		while ( HaveTypeahead() )
			ConfusionText(PopUserText());

		ConfusionText(line);
		}
	}

void LoginConn::ConfusionText(const char* line)
	{
	if ( login_confused_text )
		{
		val_list* vl = new val_list;
		vl->append(BuildConnVal());
		vl->append(new StringVal(line));
		ConnectionEvent(login_confused_text, vl);
		}
	}

int LoginConn::IsPloy(const char* line)
	{
	if ( IsLoginPrompt(line) || IsFailureMsg(line) || IsSuccessMsg(line) ||
	     IsSkipAuthentication(line) )
		{
		saw_ploy = 1;
		Confused("possible_login_ploy", line);
		return 1;
		}
	else
		return 0;
	}

int LoginConn::IsSkipAuthentication(const char* line) const
	{
	return re_skip_authentication->MatchAnywhere(line);
	}

const char* LoginConn::IsLoginPrompt(const char* line) const
	{
	int prompt_match = re_login_prompts->MatchAnywhere(line);
	if ( ! prompt_match || IsFailureMsg(line) )
		// IRIX can report "login: ERROR: Login incorrect"
		return 0;

	return &line[prompt_match];
	}

int LoginConn::IsDirectLoginPrompt(const char* line) const
	{
	return re_direct_login_prompts->MatchAnywhere(line);
	}

int LoginConn::IsFailureMsg(const char* line) const
	{
	return re_login_failure_msgs->MatchAnywhere(line) &&
		! re_login_non_failure_msgs->MatchAnywhere(line);
	}

int LoginConn::IsSuccessMsg(const char* line) const
	{
	return re_login_success_msgs->MatchAnywhere(line);
	}

int LoginConn::IsTimeout(const char* line) const
	{
	return re_login_timeouts->MatchAnywhere(line);
	}

int LoginConn::IsEmpty(const char* line) const
	{
	while ( *line && isspace(*line) )
		++line;

	return *line == '\0';
	}

void LoginConn::AddUserText(const char* line)
	{
	if ( num_user_text >= MAX_USER_TEXT )
		Confused("excessive_typeahead", line);
	else
		{
		if ( ++user_text_last == MAX_USER_TEXT )
			user_text_last = 0;

		user_text[user_text_last] = copy_string(line);

		++num_user_text;
		}
	}

char* LoginConn::PeekUserText() const
	{
	if ( num_user_text <= 0 )
		Internal("underflow in LoginConn::PeekUserText()");

	return user_text[user_text_first];
	}

char* LoginConn::PopUserText()
	{
	char* s = PeekUserText();

	if ( ++user_text_first == MAX_USER_TEXT )
		user_text_first = 0;

	--num_user_text;

	return s;
	}

Val* LoginConn::PopUserTextVal()
	{
	char* s = PopUserText();
	BroString* bs = new BroString(1, byte_vec(s), strlen(s));
	return new StringVal(bs);
	}

int LoginConn::MatchesTypeahead(const char* line) const
	{
	for ( int i = user_text_first, n = 0; n < num_user_text; ++i, ++n )
		{
		if ( i == MAX_USER_TEXT )
			i = 0;

		if ( streq(user_text[i], line) )
			return 1;
		}

	return 0;
	}

void LoginConn::FlushEmptyTypeahead()
	{
	while ( HaveTypeahead() && IsEmpty(PeekUserText()) )
		delete PopUserText();
	}

RE_Matcher* init_RE(ListVal* l)
	{
	RE_Matcher* re = l->BuildRE();
	if ( re )
		re->Compile();

	return re;
	}
