From dd8102f6e74831fdfd1317ee269666097eb24ba8 Mon Sep 17 00:00:00 2001 From: sauerbraten Date: Mon, 19 Apr 2021 21:40:58 +0200 Subject: [PATCH] add support for third-party auth servers (#75) * add support for third-party auth servers integrates p1xbraten's authservers.patch, except that the master server remains independent and is not treated as "just another auth server". adds the 'addauthserver' command. * decrease diff to vanilla fpsgame/server.cpp --- src/Makefile | 3 +- src/engine/server.cpp | 36 +++++++ src/fpsgame/server.cpp | 87 ++++++++-------- src/mod/authservers.cpp | 174 ++++++++++++++++++++++++++++++++ src/mod/authservers.h | 217 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 477 insertions(+), 40 deletions(-) create mode 100644 src/mod/authservers.cpp create mode 100644 src/mod/authservers.h diff --git a/src/Makefile b/src/Makefile index 56d5f07c..5cc9abd4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -71,7 +71,8 @@ SERVER_OBJS= \ mod/cryptomod/polarssl/library/sha1-standalone.o \ mod/cryptomod/polarssl/library/sha2-standalone.o \ mod/cryptomod/polarssl/library/sha4-standalone.o \ - mod/banlist-standalone.o + mod/banlist-standalone.o \ + mod/authservers-standalone.o # geoip ifeq ($(USE_GEOIP),true) diff --git a/src/engine/server.cpp b/src/engine/server.cpp index 4cd0305b..02b17475 100644 --- a/src/engine/server.cpp +++ b/src/engine/server.cpp @@ -7,6 +7,7 @@ #include "rconmod.h" #include "remod.h" #include "commandev.h" +#include "authservers.h" #ifdef IRC #include "irc.h" @@ -553,6 +554,15 @@ void checkserversockets() // reply all server info requests ENET_SOCKETSET_ADD(readset, mastersock); if(!masterconnected) ENET_SOCKETSET_ADD(writeset, mastersock); } + // remod: check auth servers sockets + enumerate(remod::auth::servers, remod::auth::authserver, a, { + if(a.sock != ENET_SOCKET_NULL) + { + maxsock = max(maxsock, a.sock); + ENET_SOCKETSET_ADD(readset, a.sock); + if(!a.connected) ENET_SOCKETSET_ADD(writeset, a.sock); + } + }); if(lansock != ENET_SOCKET_NULL) { maxsock = max(maxsock, lansock); @@ -598,6 +608,30 @@ void checkserversockets() // reply all server info requests } if(mastersock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, mastersock)) flushmasterinput(); } + // remod + enumerate(remod::auth::servers, remod::auth::authserver, a, { + if(a.sock != ENET_SOCKET_NULL) + { + if(!a.connected) + { + if(ENET_SOCKETSET_CHECK(readset, a.sock) || ENET_SOCKETSET_CHECK(writeset, a.sock)) + { + int error = 0; + if(enet_socket_get_option(a.sock, ENET_SOCKOPT_ERROR, &error) < 0 || error) + { + logoutf("could not connect to auth server, error: %d", error); + a.disconnect(); +} + else + { + a.connecting = 0; + a.connected = totalmillis ? totalmillis : 1; + } + } + } + if(a.sock != ENET_SOCKET_NULL && ENET_SOCKETSET_CHECK(readset, a.sock)) a.flushinput(); + } + }); } VAR(serveruprate, 0, 0, INT_MAX); @@ -659,6 +693,8 @@ void serverslice(bool dedicated, uint timeout) // main server update, called f server::serverupdate(); flushmasteroutput(); + // remod + enumerate(remod::auth::servers, remod::auth::authserver, a, { a.flushoutput(); }); checkserversockets(); if(!lastupdatemaster || totalmillis-lastupdatemaster>60*60*1000) // send alive signal to masterserver every hour of uptime diff --git a/src/fpsgame/server.cpp b/src/fpsgame/server.cpp index 5acfe144..a3773439 100644 --- a/src/fpsgame/server.cpp +++ b/src/fpsgame/server.cpp @@ -4,6 +4,7 @@ #include "commandev.h" #include "fpsgame.h" #include "remod.h" +#include "authservers.h" namespace game { @@ -1069,26 +1070,27 @@ namespace server SVAR(serverauth, ""); - struct userkey - { - char *name; - char *desc; + // remod: moved to authservers.h + // struct userkey + // { + // char *name; + // char *desc; - userkey() : name(NULL), desc(NULL) {} - userkey(char *name, char *desc) : name(name), desc(desc) {} - }; + // userkey() : name(NULL), desc(NULL) {} + // userkey(char *name, char *desc) : name(name), desc(desc) {} + // }; - static inline uint hthash(const userkey &k) { return ::hthash(k.name); } - static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } + // static inline uint hthash(const userkey &k) { return ::hthash(k.name); } + // static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } - struct userinfo : userkey - { - void *pubkey; - int privilege; + // struct userinfo : userkey + // { + // void *pubkey; + // int privilege; - userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} - ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } - }; + // userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} + // ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } + // }; hashset users; void adduser(char *name, char *desc, char *pubkey, char *priv) @@ -1137,7 +1139,8 @@ namespace server extern void connected(clientinfo *ci); - bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false) + // remod: defaults in authservers.h + bool setmaster(clientinfo *ci, bool val, const char *pass, const char *authname, const char *authdesc, int authpriv, bool force, bool trial) { if(authname && !val) return false; const char *name = ""; @@ -1220,7 +1223,8 @@ namespace server return true; } - bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false) + // remod: defaults in authservers.h + bool trykick(clientinfo *ci, int victim, const char *reason, const char *authname, const char *authdesc, int authpriv, bool trial) { int priv = ci->privilege; if(authname) @@ -2601,13 +2605,13 @@ namespace server return ci && ci->connected; } + // remod: "overriden" in authservers.cpp clientinfo *findauth(uint id) { loopv(clients) if(clients[i]->authreq == id) return clients[i]; return NULL; } - void authfailed(clientinfo *ci) { if(!ci) return; @@ -2615,11 +2619,13 @@ namespace server if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); } + // remod: "overriden" in authservers.cpp void authfailed(uint id) { authfailed(findauth(id)); } - + + // remod: "overriden" in authservers.cpp void authsucceeded(uint id) { clientinfo *ci = findauth(id); @@ -2629,21 +2635,23 @@ namespace server if(ci->authkickvictim >= 0) { if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true)) - trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); + trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); ci->cleanauthkick(); } else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH); } - + + // remod: "overriden" in authservers.cpp void authchallenged(uint id, const char *val, const char *desc = "") { clientinfo *ci = findauth(id); if(!ci) return; sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); } - + uint nextauthreq = 0; - + + // remod: "overriden" in authservers.cpp bool tryauth(clientinfo *ci, const char *user, const char *desc) { ci->cleanauth(); @@ -2654,7 +2662,7 @@ namespace server if(ci->authdesc[0]) { userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) + if(u) { uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; vector buf; @@ -2672,10 +2680,11 @@ namespace server if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); return false; } - + + // remod: "overriden" in authservers.cpp bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) { - if(ci->authreq != id || strcmp(ci->authdesc, desc)) + if(ci->authreq != id || strcmp(ci->authdesc, desc)) { ci->cleanauth(); return !ci->connectauth; @@ -2689,7 +2698,7 @@ namespace server if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) { userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) + if(u) { if(ci->connectauth) connected(ci); if(ci->authkickvictim >= 0) @@ -2700,8 +2709,8 @@ namespace server else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); } } - ci->cleanauth(); - } + ci->cleanauth(); + } else if(!requestmasterf("confauth %u %s\n", id, val)) { ci->cleanauth(); @@ -2719,7 +2728,7 @@ namespace server loopvrev(clients) { clientinfo *ci = clients[i]; - if(ci->authreq) authfailed(ci); + if(ci->authreq && !ci->authdesc[0]) authfailed(ci); // remod: check for gauth desc } } @@ -2728,11 +2737,11 @@ namespace server uint id; string val; if(sscanf(cmd, "failauth %u", &id) == 1) - authfailed(id); + remod::authfailed(id, ""); // remod else if(sscanf(cmd, "succauth %u", &id) == 1) - authsucceeded(id); + remod::authsucceeded(id, ""); // remod else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) - authchallenged(id, val); + remod::authchallenged(id, val, ""); // remod else if(matchstring(cmd, cmdlen, "cleargbans")) gbans.clear(); else if(sscanf(cmd, "addgban %100s", val) == 1) @@ -2834,7 +2843,7 @@ namespace server int disc = allowconnect(ci, password); if(disc) { - if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc)) + if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !remod::tryauth(ci, authname, authdesc)) // remod { disconnect_client(sender, disc); return; @@ -2851,7 +2860,7 @@ namespace server getstring(desc, p, sizeof(desc)); uint id = (uint)getint(p); getstring(ans, p, sizeof(ans)); - if(!answerchallenge(ci, id, ans, desc)) + if(!remod::answerchallenge(ci, id, ans, desc)) // remod { disconnect_client(sender, ci->connectauth); return; @@ -3597,7 +3606,7 @@ namespace server string desc, name; getstring(desc, p, sizeof(desc)); getstring(name, p, sizeof(name)); - tryauth(ci, name, desc); + remod::tryauth(ci, name, desc); // remod break; } @@ -3616,7 +3625,7 @@ namespace server if(u) authpriv = u->privilege; else break; } if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text); - else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc)) + else if(trykick(ci, victim, text, name, desc, authpriv, true) && remod::tryauth(ci, name, desc)) // remod { ci->authkickvictim = victim; ci->authkickreason = newstring(text); @@ -3630,7 +3639,7 @@ namespace server getstring(desc, p, sizeof(desc)); uint id = (uint)getint(p); getstring(ans, p, sizeof(ans)); - answerchallenge(ci, id, ans, desc); + remod::answerchallenge(ci, id, ans, desc); // remod break; } diff --git a/src/mod/authservers.cpp b/src/mod/authservers.cpp new file mode 100644 index 00000000..594f1082 --- /dev/null +++ b/src/mod/authservers.cpp @@ -0,0 +1,174 @@ +#include "authservers.h" + +namespace remod +{ + namespace auth + { + hashnameset servers; + + authserver *addauthserver(const char *keydomain, const char *hostname, int *port, const char *priv) + { + authserver &a = servers[keydomain]; + copystring(a.name, keydomain); + copystring(a.hostname, hostname); + a.port = *port; + switch(priv[0]) + { + case 'a': case 'A': a.privilege = PRIV_ADMIN; break; + case 'm': case 'M': default: a.privilege = PRIV_AUTH; break; + case 'n': case 'N': a.privilege = PRIV_NONE; break; + } + return &a; + } + COMMAND(addauthserver, "ssis"); + + bool requestf(const char *keydomain, const char *fmt, ...) + { + keydomain = newstring(keydomain); + authserver *a = servers.access(keydomain); + if(!a) return false; + defvformatstring(req, fmt, fmt); + return a->request(req); + } + } + + using server::clients; + using server::users; + using server::serverauth; + using server::setmaster; + + clientinfo *findauth(uint id, const char *desc) + { + loopv(server::clients) if(server::clients[i]->authreq == id && !strcmp(server::clients[i]->authdesc, desc)) return server::clients[i]; + return NULL; + } + + void authfailed(uint id, const char *desc) + { + server::authfailed(findauth(id, desc)); + } + + void authsucceeded(uint id, const char *desc) + { + clientinfo *ci = findauth(id, desc); + if(!ci) return; + int authserverprivilege = PRIV_AUTH; + if(desc[0]) { + auth::authserver *a = auth::servers.access(desc); + if(!a) return; + authserverprivilege = a->privilege; + } + ci->cleanauth(ci->connectauth!=0); + if(ci->connectauth) server::connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, ci->authdesc, authserverprivilege, false, true)) + server::trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, authserverprivilege); + ci->cleanauthkick(); + } + else if(ci->privilege >= authserverprivilege) + { + string msg; + if(desc && desc[0]) formatstring(msg, "%s authenticated as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), ci->authname, desc); + else formatstring(msg, "%s authenticated as '\fs\f5%s\fr'", colorname(ci), ci->authname); + sendf(-1, 1, "ris", N_SERVMSG, msg); + } + else setmaster(ci, true, "", ci->authname, ci->authdesc, authserverprivilege); + } + + void authchallenged(uint id, const char *val, const char *desc) + { + clientinfo *ci = findauth(id, desc); + if(!ci) return; + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); + } + + uint nextauthreq = 0; + + bool tryauth(clientinfo *ci, const char *user, const char *desc) + { + ci->cleanauth(); + if(!nextauthreq) nextauthreq = 1; + ci->authreq = nextauthreq++; + filtertext(ci->authname, user, false, false, 100); + copystring(ci->authdesc, desc); + if(desc[0] && !strcmp(desc, serverauth)) + { + server::userinfo *u = server::users.access(server::userkey(ci->authname, ci->authdesc)); + if(u) + { + uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; + vector buf; + ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf); + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf()); + } + else ci->cleanauth(); + } + else if(desc[0] ? !auth::requestf(desc, "reqauth %u %s\n", ci->authreq, ci->authname) : !requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + if(ci->authreq) return true; + if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); + return false; + } + + bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) + { + if(ci->authreq != id || strcmp(ci->authdesc, desc)) + { + ci->cleanauth(); + return !ci->connectauth; + } + for(char *s = val; *s; s++) + { + if(!isxdigit(*s)) { *s = '\0'; break; } + } + if(desc[0] && !strcmp(desc, serverauth)) + { + if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) + { + server::userinfo *u = users.access(server::userkey(ci->authname, ci->authdesc)); + if(u) + { + if(ci->connectauth) server::connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true)) + server::trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege); + } + else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); + } + } + ci->cleanauth(); + } + else if(desc[0] ? !auth::requestf(desc, "confauth %u %s\n", id, val) : !requestmasterf("confauth %u %s\n", id, val)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + return ci->authreq || !ci->connectauth; + } + + void authserverdisconnected(const char *keydomain) + { + loopvrev(clients) + { + clientinfo *ci = clients[i]; + if(ci->authreq && !strcmp(ci->authdesc, keydomain)) server::authfailed(ci); + } + } + + void processauthserverinput(const char *desc, const char *cmd, int cmdlen, const char *args) + { + uint id; + string val; + if(sscanf(cmd, "failauth %u", &id) == 1) + authfailed(id, desc); + else if(sscanf(cmd, "succauth %u", &id) == 1) + authsucceeded(id, desc); + else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) + authchallenged(id, val, desc); + } +} \ No newline at end of file diff --git a/src/mod/authservers.h b/src/mod/authservers.h new file mode 100644 index 00000000..8a1a1afb --- /dev/null +++ b/src/mod/authservers.h @@ -0,0 +1,217 @@ +#include "fpsgame.h" + +extern ENetAddress serveraddress; + +// stuff defined in vanilla code that we need in authservers.cpp, but may also be needed in fpsgame/server.cpp +namespace server +{ + extern char *serverauth; + struct userkey + { + char *name; + char *desc; + + userkey() : name(NULL), desc(NULL) {} + userkey(char *name, char *desc) : name(name), desc(desc) {} + }; + + static inline uint hthash(const userkey &k) { return ::hthash(k.name); } + static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } + + struct userinfo : userkey + { + void *pubkey; + int privilege; + + userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} + ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } + }; + extern hashset users; + void authfailed(clientinfo *ci); + bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false); + bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false); + void connected(clientinfo *ci); +} + +namespace remod +{ + extern void processauthserverinput(const char *desc, const char *cmd, int cmdlen, const char *args); + + namespace auth + { + static inline bool resolverwait(const char *name, ENetAddress *address) + { + return enet_address_set_host(address, name) >= 0; + } + + struct authserver + { + string name; // = key domain + string hostname; + int port, privilege; + ENetAddress address; + ENetSocket sock; + int connecting, connected, lastconnect, lastupdate; + vector in, out; + int inpos, outpos; + + authserver() : port(0), privilege(PRIV_NONE), + #ifdef __clang__ + address((ENetAddress){ENET_HOST_ANY, ENET_PORT_ANY}), + #else + address({ENET_HOST_ANY, ENET_PORT_ANY}), + #endif + sock(ENET_SOCKET_NULL), connecting(0), connected(0), lastconnect(0), lastupdate(0), inpos(0), outpos(0) + { + name[0] = hostname[0] = 0; + } + + void disconnect() + { + if(sock != ENET_SOCKET_NULL) + { + enet_socket_destroy(sock); + sock = ENET_SOCKET_NULL; + } + + out.setsize(0); + in.setsize(0); + outpos = inpos = 0; + + address.host = ENET_HOST_ANY; + address.port = ENET_PORT_ANY; + + lastupdate = connecting = connected = 0; + } + + void connect() + { + if(sock!=ENET_SOCKET_NULL) return; + if(!hostname[0]) return; + if(address.host == ENET_HOST_ANY) + { + if(isdedicatedserver()) logoutf("looking up %s...", hostname); + address.port = port; + if(!resolverwait(hostname, &address)) return; + } + sock = enet_socket_create(ENET_SOCKET_TYPE_STREAM); + if(sock == ENET_SOCKET_NULL) + { + if(isdedicatedserver()) logoutf("could not open socket for auth server %s", name); + return; + } + if(serveraddress.host == ENET_HOST_ANY || !enet_socket_bind(sock, &serveraddress)) + { + enet_socket_set_option(sock, ENET_SOCKOPT_NONBLOCK, 1); + if(!enet_socket_connect(sock, &address)) return; + } + enet_socket_destroy(sock); + if(isdedicatedserver()) logoutf("could not connect to auth server %s", name); + sock = ENET_SOCKET_NULL; + return; + } + + bool request(const char *req) + { + if(sock == ENET_SOCKET_NULL) + { + connect(); + if(sock == ENET_SOCKET_NULL) return false; + lastconnect = connecting = totalmillis ? totalmillis : 1; + } + + if(out.length() >= 4096) return false; + + out.put(req, strlen(req)); + return true; + } + + bool requestf(const char *fmt, ...) + { + defvformatstring(req, fmt, fmt); + return request(req); + } + + void processinput() + { + if(inpos >= in.length()) return; + + char *input = &in[inpos], *end = (char *)memchr(input, '\n', in.length() - inpos); + while(end) + { + *end = '\0'; + + const char *args = input; + while(args < end && !iscubespace(*args)) args++; + int cmdlen = args - input; + while(args < end && iscubespace(*args)) args++; + + processauthserverinput(name, input, cmdlen, args); + + end++; + inpos = end - in.getbuf(); + input = end; + end = (char *)memchr(input, '\n', in.length() - inpos); + } + + if(inpos >= in.length()) + { + in.setsize(0); + inpos = 0; + } + } + + void flushoutput() + { + if(connecting && totalmillis - connecting >= 60000) + { + logoutf("could not connect to auth server %s", name); + disconnect(); + } + if(out.empty() || !connected) return; + + ENetBuffer buf; + buf.data = &out[outpos]; + buf.dataLength = out.length() - outpos; + int sent = enet_socket_send(sock, NULL, &buf, 1); + if(sent >= 0) + { + outpos += sent; + if(outpos >= out.length()) + { + out.setsize(0); + outpos = 0; + } + } + else disconnect(); + } + + void flushinput() + { + if(in.length() >= in.capacity()) + in.reserve(4096); + + ENetBuffer buf; + buf.data = in.getbuf() + in.length(); + buf.dataLength = in.capacity() - in.length(); + int recv = enet_socket_receive(sock, NULL, &buf, 1); + if(recv > 0) + { + in.advance(recv); + processinput(); + } + else disconnect(); + } + }; + + extern hashnameset servers; + } + + using server::clientinfo; + + void authfailed(uint id, const char *desc); + void authsucceeded(uint id, const char *desc); + void authchallenged(uint id, const char *val, const char *desc); + bool tryauth(clientinfo *ci, const char *user, const char *desc); + bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc); +}