// $Id: active.c,v 1.404 2008/10/26 17:24:57 lynx Exp $ // vim:syntax=lpc:ts=8

// a jabber thing which actively connects something
#define NO_INHERIT
#include "jabber.h"
#undef NO_INHERIT
#include <uniform.h>

#ifdef ERQ_WITHOUT_SRV
# define hostname host	// hostname contains the name before SRV resolution
#endif

inherit NET_PATH "jabber/mixin_render";

// a derivate of circuit which knows JABBER
inherit NET_PATH "circuit";
inherit NET_PATH "name";

#if 0	// apparently unused
// virtual inherit of textc.c thus output.c isnt really working
#define NO_INHERIT
#include <text.h>
#undef NO_INHERIT
#endif

#include "interserver.c" // interserver stuff

volatile mixed gateways;
volatile mixed *dialback_queue;

volatile string streamid;
volatile float streamversion;
volatile int authenticated;
volatile int ready; // finished with sasl, starttls and such
volatile int dialback_outgoing;

tls_logon(); // prototype

// not strictly necessary, but more spec conformant
quit() {
	emitraw("</stream:stream>");
#ifdef _flag_log_sockets_XMPP
	log_file("RAW_XMPP", "\n%O: shutting down, quit called\n", ME);
#endif
	remove_interactive(ME);	// not XEP-0190 conformant, but shouldn't
				// matter on an outgoing-only socket
	//destruct(ME);
}

sGateway(gw, ho, id) {
    // TODO: ho is obsolete
    P2(("%O: setting %O as gateway for %O, id %O\n", ME, gw, ho, id))
    unless (gateways) gateways = ([ ]);
    gateways[id] = gw; 
    return ME; // set myself as active for this gateway
}

removeGateway(gw, id) {
    if (gateways[id] == gw) {
	m_delete(gateways, id);
    }
}

#ifdef BETA
static connectTimeOut() {
	// should we do something more or else here?
	remove_interactive(ME);
	connect_failure("_timeout_dialback",
		       	"no dialback response received, timeout");
}
#endif

start_dialback() {
    string source_host, key;

    source_host = NAMEPREP(_host_XMPP);
    key = DIALBACK_KEY(streamid, hostname, source_host);

    P3(("%O: starting dialback from %O to %O\n", ME, source_host, hostname))
    
    dialback_outgoing = 1;
    emit(sprintf("<db:result to='%s' from='%s'>%s</db:result>",
		 hostname, source_host, key));
#ifdef BETA
    call_out(#'connectTimeOut, 120);
#endif
}

process_dialback_queue() {
    mixed *t;
    ready = 1;
    if (pointerp(dialback_queue)) 
	foreach (t : dialback_queue) 
	    render(t[1], t[2], t[3], t[0]);
    dialback_queue = ({ });
}

handle_starttls(XMLNode node) {
    if (node["@xmlns"] != NS_XMPP "xmpp-tls") {
	P0(("%O: expecting xmpp-tls proceed or failure, got %O:%O\n",
	    ME, node[Tag], node["@xmlns"]))
#ifdef _flag_log_sockets_XMPP
	log_file("RAW_XMPP", "\n%O: expecting xmpp-tls proceed or failure, got %O:%O\t%O", ME, node[Tag], node["@xmlns"], ctime());
#endif
	nodeHandler = #'jabberMsg;
	return jabberMsg(node);
    }
    nodeHandler = #'jabberMsg;
    if (node[Tag] == "proceed") {
	tls_init_connection(ME, #'tls_logon);
    } else { // treat everything else as a failure
	P0(("%O got a tls failure\n", ME))
    }
}

handle_stream_features(XMLNode node) {
    unless (node[Tag] == "stream:features") {
	// this should not happen!
	P0(("%O: expecting stream:features, got %O\n", ME, node[Tag]))
	nodeHandler = #'jabberMsg;
	// TODO
	// be careful, usually there are not many nodes which an active 
	// may get, mostly errors, so it might not be a good idea
	// to do dialback if this happens

	start_dialback(); 
	process_dialback_queue();
#ifdef _flag_log_sockets_XMPP
	log_file("RAW_XMPP", "\nprocessing dialback queue, expecting stream features but not found");
#endif
	return jabberMsg(node);
    }
    unless (streamversion >= 1.0) {
	P0(("%O unexpected stream:features with stream:version %O\n",
	    ME, streamversion))
	return;
    }
    nodeHandler = #'jabberMsg;
#ifdef WANT_S2S_TLS
    // should only happen if stream version is >= "1.0"
    if (node["/starttls"] && tls_available() 
	    && !tls_query_connection_state(ME)
	    // dont starttls to hosts that are known to have 
	    // invalid tls certificates
//		&& !config(XMPP + hostname, "_tls_invalid")
       ){
	// may use tls unless we already do so
	emitraw("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
	nodeHandler = #'handle_starttls;
	return;
    }
#else
    if (node["/starttls"] 
	    && node["/starttls"]["/required"]) {
	P0(("%O requires <starttls/> but we can't provide that\n", ME))
	connect_failure("_encrypt_necessary", "Encryption required but unable to provide that");
	return;
    }
#endif
#ifdef WANT_S2S_SASL
    // possibly authenticated via sasl?
    if (authenticated && !node["/switch"]) {
	P2(("%O running Q after getting stream:features and having "
	    "authenticated via sasl\n", ME))
	process_dialback_queue();
	return runQ();
    }
    if (node["/mechanisms"] 
	    && node["/mechanisms"]["/mechanism"]){
	XMLNode tx;
	mixed mechs = ([ ]);

	// get the mechanismsm
	tx = node["/mechanisms"]["/mechanism"];

	if (nodelistp(tx)) 
	    foreach (XMLNode tx2 : tx) 
		mechs[tx2[Cdata]] = 1;
	else 
	    mechs[tx[Cdata]] = 1;

	// choose a mechanism
	P2(("available SASL mechanisms: %O\n", mechs))

#ifndef _flag_disable_authentication_external_XMPP
	if (mechs["EXTERNAL"]) {
	    // TODO we should check that the name in our 
	    // certificate is equal to _host_XMPP
	    // but so should the other side!
	    emit("<auth mechanism='EXTERNAL' "
		 "xmlns='" NS_XMPP "xmpp-sasl'>" +
		 encode_base64(_host_XMPP)
		 + "</auth>");
	    return;
	} 
#endif
    }
#endif
#ifdef SWITCH2PSYC
    else if (node["/switch"]) {	// should check scheme
	PT(("upgrading %O from XMPP to PSYC.\n", ME))
	emitraw("<switching xmlns='http://switch.psyced.org'>"
		"<scheme>psyc</scheme>"
	     "</switching>");
	return;
    }
#endif
    // send dialback request (step 4) here if nothing else
    // is done
    if (dialback_outgoing == 0 && qSize(me)) {
	start_dialback();
    }
    process_dialback_queue();
}

disconnected(remainder) {
    P2(("active %O disconnected\n", ME))
#ifdef _flag_log_sockets_XMPP
    log_file("RAW_XMPP", "\n%O disc\t%O", ME, ctime());
#endif
    authenticated = 0;
    ready = 0;
    if (dialback_outgoing != 0) {
	// just saw that happen twice with jabber.org
	// should we trigger a reconnect then?
	P0(("%O got disconnected with dialback outgoing != 0\n", ME))
	// TODO: reconnect koennte sinnvoll sein
#ifdef _flag_log_sockets_XMPP
	log_file("RAW_XMPP", "\n%O disconnected with dialback outgoing != 0\t%O", ME, ctime());
#endif
	dialback_outgoing = 0;
    }
    // nothing else happening here? no reconnect?
    // TODO: what about the dialback Q if any?
    ::disconnected(remainder);
    // let's call this a special case of good will:
    // hopefully a sending side socket close operation
    if (remainder == "</stream:stream>") return 1;
    // we could forward remainder to feed(), but we haven't seen any other
    // cases of content than the one above.
    return flags & TCP_PENDING_DISCONNECT;
}

// we love multiple inheritance.. rock'n'roll!
static int logon(int failure) {
    // TODO: it seems that it is bad to call this from tls_logon
    // 	 for a second time
// 1. Originating Server establishes TCP connection to Receiving Server.
    if (NET_PATH "circuit"::logon(failure)) {
	set_combine_charset(COMBINE_CHARSET);
#ifdef INPUT_NO_TELNET
	input_to(#'feed, INPUT_IGNORE_BANG | INPUT_CHARMODE | INPUT_NO_TELNET);
#else
	enable_telnet(0, ME);
	input_to(#'feed, INPUT_IGNORE_BANG | INPUT_CHARMODE);
#endif
	sTextPath(0, "en", "jabber");
#ifdef _flag_log_sockets_XMPP
	log_file("RAW_XMPP", "\n%O logon\t%O", ME, ctime());
#endif
/* 2. Originating Server sends a stream header to Receiving Server */
	emit("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
	     "xmlns='jabber:server' xmlns:db='jabber:server:dialback' "
	     "to='" + hostname + "' "
	     "from='" + NAMEPREP(_host_XMPP) + "' "
	     "xml:lang='en' "
	     "version='1.0'>");
#if 1 // not strictly necessary, but more spec conformant
    } else if (!qSize(me)) { // no retry for dialback-only
	//if (sizeof(dialback_queue) > 1) {
	    P2(("%O failed to connect. dialback queue is %O\n",
	       	ME, dialback_queue))
	//}
	if (sizeof(gateways)) {
	    foreach(string id, mixed gw : gateways) {
		if (objectp(gw)) {
		    P2(("%O notifies %O of failure (%O).\n", ME, gw, id))
		    gw->remote_connection_failed();
		}
	    }
	}
	dialback_queue = 0;
	destruct(ME);
#endif
    }
    unless (gateways) {
	gateways = ([ ]);
    }
    return 1;
}

#ifdef WANT_S2S_TLS
tls_logon(result) {
    if (result < 0) {
	P1(("%O tls_logon %d: %O\n", ME, result, tls_error(result) ))
	// would be nice to insert the tls_error() message here.. TODO
	connect_failure("_encrypt", "Problems setting up an encrypted circuit");
    } else if (result == 0) {
	// we need to check the certificate
	// selfsigned certs are ok, but we need dialback then
	//
	// if the cert is ok, we can set authenticated to 1
	// to skip dialback
	mixed cert = tls_certificate(ME, 0);
	P3(("active::certinfo %O\n", cert))
	if (mappingp(cert)) {
	    unless (certificate_check_jabbername(hostname, cert)) {
#ifdef _flag_report_bogus_certificates
		monitor_report("_error_invalid_certificate_identity",
			       sprintf("%O presented a certificate that "
				       "contains %O/%O",
				       hostname, cert["2.5.4.3"],
				       cert["2.5.29.17:1.3.6.1.5.5.7.8.5"]));
#endif
#ifdef _flag_log_bogus_certificates
		log_file("CERTS", S("%O %O %O id?\n", ME, hostname, cert));
#else
		P1(("TLS: %s presented a certificate with unexpected identity.\n", hostname))
		P2(("%O\n", cert))
#endif
#if 0 //def _flag_reject_bogus_certificates
		QUIT
		return 1;
#endif
	    } 
	    else if (cert[0] != 0) {
#ifdef _flag_report_bogus_certificates
		monitor_report("_error_untrusted_certificate",
			       sprintf("%O certificate could not be verified",
				       hostname));
#endif
#ifdef _flag_log_bogus_certificates
		log_file("CERTS", S("%O %O %O\n", ME, hostname, cert));
#else
		P1(("TLS: %s presented untrusted certificate.\n", hostname))
		P2(("%O\n", cert))
#endif
#if 0 //def _flag_reject_bogus_certificates
		// QUIT is wrong...
		QUIT
		return 1;
#endif
	    }
	} else {
	    P0(("%O: no tls_certificate() for %O\n", ME, hostname))
	}
	logon(0);
    } else {
	// should not happen, if it does the driver is doing sth wrong
	PT(("%O tls_logon %d - MUST NOT HAPPEN\n", ME, result))
    }
}
#endif

// this one is completly specific to active.c
jabberMsg(XMLNode node) {
    /* this will be rarely used if we do dialback
     * due to active part beeing virtually write-only
     */
    P2(("%O jabber/active got %O\n", ME, node[Tag])) 
    object o;
    string t;
    switch (node[Tag]) {
    case "db:result":
	/* 10: Receiving Server informs Originating Server of the result
	 * we are originating server and are informed of the result
	 */
	dialback_outgoing = 0;
#ifdef BETA
    	remove_call_out(#'connectTimeOut);
#endif
	if (node["@type"] == "valid") {
#ifdef LOG_XMPP_AUTH
	    D0( log_file("XMPP_AUTH", "\n%O auth dialback", ME); )
#endif 
	    authenticated = 1;
	    runQ();
	} else {
	    // else: queue failed
	    P0(("%O something gone wrong in active db:result: %O\n", ME, node))
	    authenticated = 0;
	    /* Note: At this point, the connection has either been 
	     * validated via a type='valid', or reported as invalid.
	     * If the connection is invalid, then the Receiving 
	     * Server MUST terminate both the XML stream and the 
	     * underlying TCP connection. 
	     */
	    emitraw("</stream:stream>");
	    remove_interactive(ME);
	    connect_failure("_dialback", "dialback gone wrong");
	}
	break;
    case "db:verify": // receiving step 9
	// t = NAMEPREP(node["@to"]) + ";" + node["@id"];
	t = node["@id"];
	o = gateways[t];
	if (objectp(o)) {
	    o -> verify_connection(node["@to"],
				   node["@from"],
				   node["@type"]);
	    // probably we can delete this...
	    m_delete(gateways, t);
#if 1 // not strictly necessary, but more spec conformant
	} else if (member(gateways, t)) {
	    P0(("%O found gateway for %O, but it is not an object: %O\n",
		ME, t, o))
	    m_delete(gateways, t);
#endif
	} else {
	    // wir haben keinen Gateway zu der ID... 
	    // was machen wir? loggen und ignorieren!
	    P0(("%O could not find %O in %O\n", ME, 
		t, gateways));
	}
	break;
#ifdef WANT_S2S_SASL
    case "success":
	if (node["@xmlns"] == NS_XMPP "xmpp-sasl") {
# ifdef LOG_XMPP_AUTH
	    D0( log_file("XMPP_AUTH", "\n%O auth SASL", ME); )
# endif 
	    // TODO: for digest-md5 we should check rspauth
	    emit("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' "
		 "xmlns='jabber:server' xmlns:db='jabber:server:dialback' "
		 "to='" + hostname + "' "
		 "from='" + NAMEPREP(_host_XMPP) + "' "
		 "xml:lang='en' "
		 "version='1.0'>");
	    authenticated = 1;
	}
	break;
    case "failure":
	// the other side has to close the stream
	monitor_report("_error_invalid_authentication_XMPP", sprintf("%O got a failure with xml namespace %O\n", ME, node["@xmlns"]));
	connect_failure("_invalid_authentication", "counterpart did not like our authorization");
	break;
#endif
    case "stream:error":
	if (ME) remove_interactive(ME);
	authenticated = 0;
	if (node["/connection-timeout"]) {
	    // this is normal, unless we had something to say,
	    // but we are an event-oriented system, so this
	    // "if" should never happen. then again, i've seen
	    // plenty of should-never-happens to happen, so..
	    // to put it in brian wilson words, god only knows  ;)
	    if (qSize(me)) 
		connect_failure("_timeout", "counterpart sent timeout");
	} else if (node["/not-authorized"]) {
	    connect_failure("_illegal_source",
			    "counterpart claims we are not authorized");
	} else if (node["/system-shutdown"]) {
	    connect_failure("_unavailable_restart",
			    "counterpart is doing a system shutdown");
	} else if (node["/host-unknown"]) {
	    // remote side runs a jabber server, but does not
	    // accept hostname
	    connect_failure("_host_unknown",
			    "counterpart does not recognize hostname");
	} else {
	    P0(("%O stream error, other side said %O\n", ME, node))
	    connect_failure("_unknown",
			    "counterpart stopped transaction for unknown reason");
	}
	break;
#ifdef SWITCH2PSYC
    case "switched":
# ifdef QUEUE_WITH_SCHEME
	o = ("psyc:" + host) -> circuit();
# else
	o = ("psyc:" + host) -> circuit(0, 0, 0, 0, host);  // whoami
# endif
	P1(("%O switched to %O for %O\n", ME, o, host))
	exec(o, ME);
	o -> logon();
	destruct(ME);
	return;
#endif
    default:
	P0(("%O: unexpected %O:%O\n", ME, node["@tag"], 
	    node["@xmlns"]))
	break;
    }
}

// prototype
int msg(string source, string mc, string data,
	    mapping vars, int showingLog, string target) {
    mixed t;
    string template, output;

    P2(("%O jabber/active:msg(%O,%O..)\n", ME, source, mc))
    /* TODO: use the language, i.e. sTextPath if _language is not
     * 	the current language 
     * 	sTextPath is quite expensive (string object lookup), 
     * 	but as we're parsing xml anyway...
     */ 
    vars = copy(vars);
#ifdef DEFLANG
    unless(vars["_language"]) vars["_language"] = DEFLANG;
#else
    unless(vars["_language"]) vars["_language"] = "en";
#endif
    /* another note:
     * instead of queuing the msg()-calls we could simply queue
     * the output from emit (use the new net/outputb ?)
     * this avoids bugs with destructed objects
     */
#if 0 // !EXPERIMENTAL
    /* currently, we want _status_person_absent
     * this may change...
     */
    else if (abbrev("_status_person_absent", mc)) return 1;
#endif
    switch (mc){
    case "_message_echo_private":
	return 1;
    }
    // desperate hack
    unless (vars["_INTERNAL_mood_jabber"])
	    vars["_INTERNAL_mood_jabber"] = "neutral";

    // TODO: only _dialback_request_verify sollte hier betroffen sein
    if (abbrev("_dialback", mc)) {
	unless (interactive()) connect(); // ?
	if (ready) {
	    render(mc, data, vars, source);
	} else {
	    unless (dialback_queue) dialback_queue = ({ });
	    dialback_queue += ({ ({ source, mc, data, vars, target }) });
	}
	return 1;
    }

    // this should be part of render()/w()
    // but it has to happen before enqueuing (otherwise we had
    // problems with destructed objects)
    // "late enqueu" could be a solution to that problem
    determine_sourcejid(source, vars);
    determine_targetjid(target, vars);
    if (vars["_place"]) vars["_place"] = mkjid(vars["_place"]);

    // component needs to set authenticated as needed
    unless (authenticated) {
	if (interactive() && ready && dialback_outgoing == 0) {
	    start_dialback();
	}
#if _host_XMPP == SERVER_HOST
      // we can only do this if mkjid patches the psyc host into jabber host
      // but that is terrifically complicated and requires parsing of things
      // we already knew.. so let's simply enqueue with object id and reject
      // if the object got lost in the meantime
	vars["_source"] = UNIFORM(source);
	// hmm.. actually no.. since we also set _INTERNAL_source_jabber
	// this variable should never get used for anything anyway.. so
	// leaving it out should be completely harmless
#else
	// is this a bad idea? not sure.. we'll see..
	// if i don't have this here, a "tell lynX where it happened" will
	// be triggered from here -- best to never use _host_XMPP really ;)
	vars["_source"] = source;
	// behaviour has changed.. so maybe we don't need this any longer TODO
#endif
	return enqueue(source, mc, data, vars, showingLog, target);
    }
    return ::msg(source, mc, data, vars, showingLog, target);
}

open_stream(XMLNode node)
{
    mixed *t;
    string key;

    P2(("%O active for %O.\n", ME, hostname))
    D1( unless (interactive(ME))
	PP(("%O open_stream: should be interactive!\n", ME)); 
    )
    streamid = node["@id"];

    // actually, this is incorrect, als both parts of the version
    // are to be treated as separte ints ($4.4.1)
    // but as long as we only have 0 and 1.0...
    streamversion = to_float(node["@version"]);
    // note: flushing this Q should wait until stream features is completed	
    if (streamversion < 1.0) {
	process_dialback_queue();
	if (qSize(me)) 
	    start_dialback();
    } else { 
	// wait for stream:features
	nodeHandler = #'handle_stream_features;
    }
    return;
}
