psyced/world/net/jabber/server.c

519 lines
15 KiB
C

// $Id: server.c,v 1.159 2008/10/01 10:59:24 lynx Exp $ // vim:syntax=lpc:ts=8
#include "jabber.h"
#include "server.h" // inherits net/server
#include "person.h" // find_person
#include "uniform.h"
#include <sys/tls.h>
volatile string authtag;
volatile string resource;
volatile string streamid;
volatile string pass;
volatile string language;
int reprompt;
volatile string sasluser;
volatile string saslchallenge;
#ifdef __TLS__
volatile mixed certinfo;
#endif
qScheme() { return "jabber"; }
// qName() { } // we need that in common.c - probably no more
void create() {
unless (clonep()) return;
// not multilingual yet - TODO
sTextPath(0, 0, "jabber");
}
#ifdef __TLS__
// similar code in other files
tls_logon(result) {
if (result == 0) {
# ifdef WANT_S2S_SASL /* hey wait.. this is not S2S here!? */
certinfo = tls_certificate(ME, 0);
# endif
P3(("%O tls_logon fetching certificate: %O\n", ME, certinfo))
# ifdef ERR_TLS_NOT_DETECTED
} else if (result == ERR_TLS_NOT_DETECTED) {
// no encryption. no problem.
# endif
} else if (result < 0) {
P1(("%O TLS error %d: %O\n", ME, result, tls_error(result)))
QUIT // don't fall back to plaintext instead
} else {
P0(("tls_logon with result > 0?!?!\n"))
// should not happen
}
}
#endif
string *splitsasl(string data) {
string *result = ({ "" });
int *decoded;
int i, j;
decoded = decode_base64(data);
foreach(i : decoded) {
if (i == 0) {
j++;
result += ({ "" });
continue;
}
result[j] += sprintf("%c", i);
}
return result;
}
promptForPassword(user) {
P2(("promptForPassword with %O\n", user))
if (reprompt == 1 || pass) {
w("_error_invalid_password", "Invalid password.\n",
([ "_tag_reply" : authtag || "", "_nick" : nick,
"_resource" : resource ]) );
emitraw("</stream:stream>");
QUIT // should we really close the stream here?
return; // ?
}
P2(("nick %O, pass %O\n", nick, pass))
unless (pass) {
reprompt = 1;
w("_query_password", 0, //"Please provide your password.",
([ "_nick": nick, "_tag_reply" : authtag || "" ]) );
}
return 1;
}
logon(a) {
#ifdef _flag_log_sockets_XMPP
log_file("RAW_XMPP", "\n%O logon\t%O", ME, ctime());
#endif
P3(("logon(%O) beim jabber:server.c\n", a))
set_combine_charset(COMBINE_CHARSET);
#ifdef INPUT_NO_TELNET
input_to(#'feed, input_to_settings =
INPUT_IGNORE_BANG | INPUT_CHARMODE | INPUT_NO_TELNET);
#else
enable_telnet(0, ME);
input_to(#'feed, input_to_settings =
INPUT_IGNORE_BANG | INPUT_CHARMODE);
#endif
return ::logon(a);
}
createUser(nick) {
return named_clone(JABBER_PATH "user", nick);
}
userLogon() {
user->sTag(authtag);
user->sResource(resource);
return ::userLogon();
}
authChecked(result, varargs array(mixed) args) {
// a point where we could be sending our jabber:iq:auth reply
// instead of letting _notice_login do that
PT(("%O got authChecked %O, %O\n", ME, result, args))
return ::authChecked(result, args...);
}
jabberMsg(XMLNode node) {
XMLNode helper;
string id;
mixed t;
id = node["@id"] || ""; // tag?
switch (node[Tag]) {
case "iq":
if (node["/bind"]) {
// suppresses the jabber:iq:auth reply in the SASL case
authtag = -1;
unless (sasluser) {
// not-allowed stanza error?
return 0;
}
helper = node["/bind"];
// see XMPP-core, 7. Resource binding
if (helper["/resource"])
resource = helper["/resource"][Cdata];
// assign a resource
if (!stringp(resource) || resource == "")
resource = "PSYC";
nick = sasluser;
sasluser = ""; // why an empty string? explanation needed
emit("<iq type='result' id='"+ id +"'>"
"<bind xmlns='" NS_XMPP "xmpp-bind'><jid>"+
nick +"@" SERVER_HOST "/"+ resource +"</jid>"
"</bind></iq>");
return 0;
} else if (node["/session"]) {
unless(user) return 0; // what then?
emit("<iq type='result' id='"+ id +"' from='"
SERVER_HOST "'/>");
user -> vSet("language", language);
return morph();
}
switch (node["/query"]["@xmlns"]) {
// old-school style.. for clients that don't like SASL, like kopete, jabbin
case "jabber:iq:auth":
authtag = id;
if (node["@type"] == "get"){
// hello(nick) ?
w("_query_password", 0,
([ "_nick": nick, "_tag_reply": authtag || "" ]), "");
} else if (node["@type"] == "set") {
helper = node["/query"];
resource = helper["/resource"][Cdata];
nick = helper["/username"][Cdata];
if (mappingp(helper["/password"]) &&
(pass = helper["/password"][Cdata])) {
hello(nick, 0, pass);
} else if (mappingp(helper["/digest"]) &&
(pass = helper["/digest"][Cdata])) {
P3(("jabber:iq:auth/digest got %O\n", pass))
hello(nick, 0, pass, "sha1", streamid);
} else {
// TODO: write an error?
P0(("jabber:server iq auth set without pass or digest\n"))
}
}
return 1;
case "jabber:iq:register":
if (node["@type"] == "get"){
string packet;
#if defined(_flag_disable_unauthenticated_users) || defined(_flag_disable_registration) || defined(_flag_disable_registration_XMPP)
// super dirty.. this should all be in textdb
packet = sprintf("<iq type='result' id='%s'>"
"<query xmlns='jabber:iq:register'/>"
"<error code='501>Registration by XMPP not permitted.</error></iq>",
id);
#else
packet = sprintf("<iq type='result' id='%s'>"
"<query xmlns='jabber:iq:register'>"
"<instructions>You dont even need to register, "
"this is psyced. Manual at http://help.pages.de</instructions>"
"<name/><email/><username/><password/></query></iq>",
id);
#endif
emit(packet);
} else if (node["@type"] == "set"){
string packet;
packet = sprintf("<iq type='error' id='%s'>"
"<query xmlns='jabber:iq:register'>",
id);
unless ((helper = node["/query"])
&& (t = helper["/username"][Cdata])
&& (user = summon_person(t))) {
// internal server error
STREAM_ERROR("internal-server-error",
"Oh dear! Internal server error")
// QUIT
}
unless (user -> isNewbie()) {
// already registered to someone else
packet += "</query><error code='406' type='cancel'>"
"<conflict xmlns='" NS_XMPP "xmpp-stanzas'/>"
"</error></iq>";
emit(packet);
// elmex says, disconnect is wrong here:
// QUIT
// it should at least close the stream
} else unless ((t = helper["/username"]) &&
t[Cdata] &&
(t = helper["/password"]) &&
t[Cdata]) {
packet += "</query><error code='406' type='modify'>"
"<not-acceptable xmlns='" NS_XMPP "xmpp-stanzas'/>"
"</error></iq>";
emit(packet);
// QUIT
} else {
#if defined(_flag_disable_unauthenticated_users) || defined(_flag_disable_registration) || defined(_flag_disable_registration_XMPP)
// TODO: generate some error as above
#else
user -> vSet("password", t[Cdata]);
if (t = helper["/email"]) {
user -> vSet("email", helper["/email"]);
}
// maybe immediate save is not really a good idea
// user -> save();
emit(sprintf("<iq type='result' id='%s'/>", id));
#endif
}
user = 0;
}
}
break;
case "starttls":
#if __EFUN_DEFINED__(tls_available)
if (tls_available()) {
emitraw("<proceed xmlns='" NS_XMPP "xmpp-tls'/>");
// we may not write until tls_logon is called!
tls_init_connection(ME, #'tls_logon);
} else {
P1(("%O received a 'starttls' but TLS isn't available.\n", ME))
}
#else
emitraw("<failure xmlns='" NS_XMPP "xmpp-tls'/></stream:stream>");
destruct(ME);
#endif
break;
case "auth":
switch (node["@mechanism"]) {
case "DIGEST-MD5":
// TODO: time-based nonce
saslchallenge = RANDHEXSTRING;
emit("<challenge xmlns='" NS_XMPP "xmpp-sasl'>" +
encode_base64(sprintf("realm=\"%s\",nonce=\"%s\","
"qop=\"auth\",charset=utf-8,"
"algorithm=md5-sess",
SERVER_HOST,
saslchallenge)
) + "</challenge>");
break;
case "PLAIN":
string *creds;
creds = splitsasl(node[Cdata]);
// check that creds[2] is valid for user creds[1]
// and in case of success:
if ((sizeof(creds) == 3)
&& (user = find_person(creds[1])
|| (user = createUser(creds[1])))
#ifdef ASYNC_AUTH
) {
mixed authCb = CLOSURE((int result), (mixed creds), (creds),
{
if (result) {
sasluser = creds[1];
emitraw("<success xmlns='" NS_XMPP "xmpp-sasl'/>");
} else {
sasluser = 0;
SASL_ERROR("temporary-auth-failure")
QUIT
}
return;
});
user -> checkPassword(creds[2], "plain", 0, 0, authCb);
#else
&& user -> checkPassword(creds[2], "plain")) {
sasluser = creds[1];
emitraw("<success xmlns='" NS_XMPP "xmpp-sasl'/>");
#endif
} else {
SASL_ERROR("invalid-mechanism")
QUIT
}
break;
#ifdef __TLS__
case "EXTERNAL":
# if 0
// TODO: basically this works but is untested due to
// lack of clients
// also, it uses email addresses instead of xmpp jids
// and i am not sure if I should check who signed the cert,
// as I probably want my users only to use certs created by me
unless (node[Cdata]) {
SASL_ERROR("incorrect-encoding")
QUIT
} else unless (mappingp(certinfo) && certinfo[0] == 0
&& certinfo["1.2.840.113549.1.9.1"]) {
SASL_ERROR("invalid-mechanism")
QUIT
} else {
string deco, ho, u;
// TODO: there could be a try-except block here with
// incorrect-encoding sasl error
deco = to_string(decode_base64(node[Cdata]));
// TODO: the right thingie could be a list!
unless (deco == certinfo["1.2.840.113549.1.9.1"]) {
// TODO: not sure about this one
SASL_ERROR("invalid-mechanism")
QUIT
}
// lets see if its one of our users
sscanf(deco, "%s@%s", u, ho);
unless (is_localhost(ho)) {
// wrong host
SASL_ERROR("invalid-authzid")
QUIT
// TODO: consider legalized names
} else unless ( ///// legal_name(u)) {
SASL_ERROR("not-authorized")
QUIT
} else {
user = find_person(u) || createUser(u);
sasluser = u;
emitraw("<success xmlns='" NS_XMPP "xmpp-sasl'/>");
}
}
# else
SASL_ERROR("invalid-mechanism")
QUIT
# endif
break;
#endif
#ifndef _flag_disable_unauthenticated_users
case "ANONYMOUS":
unless(node[Cdata]) {
SASL_ERROR("incorrect-encoding")
QUIT // i suppose this was missing here --lynX
} else {
string u;
t = to_string(decode_base64(node[Cdata]));
u = legal_name(t);
unless(u) {
if (t) {
SASL_ERROR("not-authorized")
} else {
SASL_ERROR("invalid-authzid")
}
QUIT
}
PT(("wants to auth anonymously as %O\n", u))
unless (user = summon_person(u)) {
SASL_ERROR("temporary-auth-failure")
QUIT
}
unless (user -> isNewbie()) {
SASL_ERROR("not-authorized")
QUIT
}
sasluser = u;
emitraw("<success xmlns='" NS_XMPP "xmpp-sasl'/>");
}
break;
#endif
default:
SASL_ERROR("invalid-mechanism")
QUIT
break;
}
break;
case "response":
// this does not behave according to rfc3920 but rather the 3920bis
// version
if (node[Cdata] && (t = to_string(decode_base64(node[Cdata])))) {
mapping creds = sasl_parse(t);
mixed authCb = CLOSURE((int result), (mixed creds), (creds),
{
if (result) {
P3(("digest md5 success\n"))
sasluser = creds["username"];
P3(("result is %O\n", result))
emit("<success xmlns='" NS_XMPP "xmpp-sasl'>"
+ encode_base64("rspauth=" + result) +
"</success>");
} else {
P0(("digest md5 failure: %O\n", creds))
sasluser = 0;
SASL_ERROR("invalid-authzid") // why do we get here?
QUIT
}
return 0; // ignored, but avoids a warning
});
user = find_person(creds["username"]) || createUser(creds["username"]);
user -> checkPassword(1, "digest-md5", 0, creds, authCb);
} else {
SASL_ERROR("not-authorized")
QUIT
}
break;
default:
/* angeblich werden andere Pakete in diesen Zustand gequeried
* aber das glaube ich nicht dass das wirklich so ist
* wie auch immer haetten wir sonst mittlerweile net/queue fuer
* diesen job
*/
P0(("jabber/server:jabberMsg default case\n"))
}
// return ::jabberMsg(from, cmd, args, data, all);
return 0;
}
open_stream(XMLNode node) {
string features;
string header;
P2(("%O open_stream from %O\n", ME, query_ip_name()))
streamid = RANDHEXSTRING;
unless(node["@xmlns"] == "jabber:client") {
// TODO: is this a fatal error?
}
switch (node["@xml:lang"]) {
case "DE": // what about DE_at etc?
case "de":
case "german":
case "German": // curious... isnt the xml:lang supposed to be 'de'?
language = "de";
break;
default:
if (!language) language = "en";
break;
}
features = "";
header = "<stream:stream "
"from='" SERVER_HOST "' "
"xmlns='jabber:client' "
"xmlns:stream='http://etherx.jabber.org/streams' ";
if (node["@version"] == "1.0") {
header += "version='1.0' ";
features = "<stream:features>";
#if __EFUN_DEFINED__(tls_available)
if (tls_available() && tls_query_connection_state(ME) == 0)
features += "<starttls xmlns='" NS_XMPP "xmpp-tls'/>";
#endif
if (sasluser) {
features += "<bind xmlns='" NS_XMPP "xmpp-bind'/>";
features += "<session xmlns='" NS_XMPP "xmpp-session'/>";
} else {
features += "<mechanisms xmlns='" NS_XMPP "xmpp-sasl'>"
#ifndef _flag_disable_authentication_digest_MD5
"<mechanism>DIGEST-MD5</mechanism>"
#endif
"<mechanism>PLAIN</mechanism>";
#ifndef _flag_disable_unauthenticated_users
// sasl anonymous
"<mechanism>ANONYMOUS</mechanism>";
#endif
// here it makes sense to use check_jabbername
// but that is currently unused anyway
#if __EFUN_DEFINED__(tls_available)
if (tls_available() && tls_query_connection_state(ME) > 0
&& mappingp(certinfo) && certinfo[0] == 0
// why do we use the old one here?
&& certificate_check_jabbername(0, certinfo)) {
features += "<mechanism>EXTERNAL</mechanism>";
}
#endif
features += "</mechanisms>";
features += "<auth xmlns='http://jabber.org/features/iq-auth'/>";
#ifndef _flag_disable_unauthenticated_users
features += "<register xmlns='http://jabber.org/features/iq-register'/>";
#endif
}
features += "</stream:features>";
}
header += "id='" + streamid + "'>";
emit(header + features);
}
// overrides certificate_check_jabbername from common.c with a function
// that is approproate for authenticating users
certificate_check_jabbername(name, certinfo) {
// plan: prefer subjectAltName:id-on-xmppAddr,
// but allow email (1.2.840.113549.1.9.1)
// and subjectAltName:rfc822Name
// FIXME: do something useful here...
return 0;
}