// $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 #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 #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(""); #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); } } 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("%s", hostname, source_host, key)); } 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(""); nodeHandler = #'handle_starttls; return; } #else if (node["/starttls"] && node["/starttls"]["/required"]) { P0(("%O requires 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("" + encode_base64(_host_XMPP) + ""); return; } else #endif if (mechs["DIGEST-MD5"] && config(XMPP + hostname, "_secret_shared")) { PT(("jabber/active requesting to do digest md5\n")) emit("" "psyc" ""); 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 == "") 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(""); #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; 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(""); 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(""); authenticated = 1; } break; case "challenge": PT(("%O got a sasl challenge\n", ME)) if (node["@xmlns"] == NS_XMPP "xmpp-sasl") { unless(t = node[Cdata]) { // none given } else unless (t = to_string(decode_base64(t))) { // base64 decode error? } else { // this one is shared across all those digest md5's mixed data; string secret; string response; PT(("decoded challenge: %O\n", t)) data = sasl_parse(t); PT(("extracted %O\n", data)) data["username"] = _host_XMPP; secret = config(XMPP + hostname, "_secret_shared"); unless(secret) { // mh... this is a problem! // we only started doing this if we have a secret, // so this cant be empty } data["cnonce"] = RANDHEXSTRING; data["nc"] = "00000001"; data["digest-uri"] = "xmpp/" _host_XMPP; response = sasl_calculate_digestMD5(data, secret, 0); // ok, the username is our hostname // note: qop must not be quoted, as we are 'client' t = "username=\"" _host_XMPP "\"," "realm=\"" + data["realm"] + "\"," "nonce=\"" + data["nonce"] + "\"," "cnonce=\"" + data["cnonce"] + "\"," "nc=" + data["nc"] + ",qop=auth," "digest-uri=\"" + data["digest-uri"] + "\"," "response=" + response + ",charset=utf-8"; PT(("%O sent rspauth %O\n", ME, response)) emit("" + encode_base64(t) + ""); } } 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; }