diff options
Diffstat (limited to 'bouncer')
-rw-r--r-- | bouncer/buffer.h | 197 | ||||
-rwxr-xr-x | bouncer/build.sh | 9 | ||||
-rwxr-xr-x | bouncer/build_static.sh | 9 | ||||
-rw-r--r-- | bouncer/client.cpp | 331 | ||||
-rw-r--r-- | bouncer/core.h | 373 | ||||
-rw-r--r-- | bouncer/dns.cpp | 159 | ||||
-rw-r--r-- | bouncer/dns.h | 15 | ||||
-rw-r--r-- | bouncer/ircserver.cpp | 393 | ||||
-rw-r--r-- | bouncer/main.cpp | 61 | ||||
-rw-r--r-- | bouncer/mobileclient.cpp | 77 | ||||
-rw-r--r-- | bouncer/netcore.cpp | 330 | ||||
-rw-r--r-- | bouncer/server.cpp | 160 | ||||
-rw-r--r-- | bouncer/socketcommon.cpp | 174 | ||||
-rw-r--r-- | bouncer/window.cpp | 468 |
14 files changed, 2756 insertions, 0 deletions
diff --git a/bouncer/buffer.h b/bouncer/buffer.h new file mode 100644 index 0000000..bfba94a --- /dev/null +++ b/bouncer/buffer.h @@ -0,0 +1,197 @@ +#ifndef BUFFER_H +#define BUFFER_H + +#include <string.h> +#include <stdint.h> + +class Buffer { +private: + char *m_data; + bool m_freeBuffer; + int m_size; + int m_capacity; + int m_readPointer; + char m_preAllocBuffer[0x200]; + +public: + Buffer() { + m_data = m_preAllocBuffer; + m_freeBuffer = false; + m_size = 0; + m_capacity = sizeof(m_preAllocBuffer); + m_readPointer = 0; + } + + ~Buffer() { + if ((m_data != NULL) && m_freeBuffer) { + delete[] m_data; + m_data = NULL; + } + } + + void useExistingBuffer(char *data, int size) { + if (m_freeBuffer) + delete[] m_data; + + m_data = data; + m_freeBuffer = false; + m_size = size; + m_capacity = size; + m_readPointer = 0; + } + + char *data() const { return m_data; } + int size() const { return m_size; } + int capacity() const { return m_capacity; } + + void setCapacity(int capacity) { + if (capacity == m_capacity) + return; + + // Trim the size down if it's too big to fit + if (m_size > capacity) + m_size = capacity; + + char *newBuf = new char[capacity]; + + if (m_data != NULL) { + memcpy(newBuf, m_data, m_size); + if (m_freeBuffer) + delete[] m_data; + } + + m_data = newBuf; + m_capacity = capacity; + m_freeBuffer = true; + } + + void clear() { + m_size = 0; + } + void append(const char *data, int size) { + if (size <= 0) + return; + + int requiredSize = m_size + size; + if (requiredSize > m_capacity) + setCapacity(requiredSize + 0x100); + + memcpy(&m_data[m_size], data, size); + m_size += size; + } + void append(const Buffer &buf) { + append(buf.data(), buf.size()); + } + void resize(int size) { + if (size > m_capacity) + setCapacity(size + 0x100); + m_size = size; + } + + void trimFromStart(int amount) { + if (amount <= 0) + return; + if (amount >= m_size) { + clear(); + return; + } + + memmove(m_data, &m_data[amount], m_size - amount); + m_size -= amount; + } + + + void writeU32(uint32_t v) { append((const char *)&v, 4); } + void writeU16(uint16_t v) { append((const char *)&v, 2); } + void writeU8(uint8_t v) { append((const char *)&v, 1); } + void writeS32(int32_t v) { append((const char *)&v, 4); } + void writeS16(int16_t v) { append((const char *)&v, 2); } + void writeS8(int8_t v) { append((const char *)&v, 1); } + + void writeStr(const char *data, int size = -1) { + if (size == -1) + size = strlen(data); + writeU32(size); + append(data, size); + } + + void readSeek(int pos) { + m_readPointer = pos; + } + int readTell() const { + return m_readPointer; + } + bool readRemains(int size) const { + if ((size > 0) && ((m_readPointer + size) <= m_size)) + return true; + return false; + } + void read(char *output, int size) { + if ((m_readPointer + size) > m_size) { + // Not enough space to read the whole thing...! + int copy = m_size - m_readPointer; + if (copy > 0) + memcpy(output, &m_data[m_readPointer], copy); + memset(&output[copy], 0, size - copy); + m_readPointer = m_size; + } else { + memcpy(output, &m_data[m_readPointer], size); + m_readPointer += size; + } + } + uint32_t readU32() { uint32_t v; read((char *)&v, 4); return v; } + uint16_t readU16() { uint16_t v; read((char *)&v, 2); return v; } + uint8_t readU8() { uint8_t v; read((char *)&v, 1); return v; } + int32_t readS32() { int32_t v; read((char *)&v, 4); return v; } + int16_t readS16() { int16_t v; read((char *)&v, 2); return v; } + int8_t readS8() { int8_t v; read((char *)&v, 1); return v; } + + void readStr(char *output, int bufferSize) { + uint32_t size = readU32(); + if (!readRemains(size)) { + strcpy(output, ""); + return; + } + + // How much can we safely get? + int readAmount; + if (size < (bufferSize - 1)) + readAmount = size; + else + readAmount = bufferSize - 1; + + // Put this into the buffer + read(output, readAmount); + output[readAmount] = 0; + + // In case the buffer was too small, skip over the extra source data + m_readPointer += (size - readAmount); + } + + + void dump() { + for (int base = 0; base < m_size; base += 0x10) { + printf("%08x | ", base); + + int pos; + for (pos = base; (pos < m_size) && (pos < (base + 0x10)); pos++) + printf("%02x ", (uint8_t)m_data[pos]); + + if (pos < (base + 0x10)) + for (; pos < (base + 0x10); pos++) + printf(" "); + + printf("| "); + + for (pos = base; (pos < m_size) && (pos < (base + 0x10)); pos++) + if (m_data[pos] >= 32) + printf("%c", m_data[pos]); + else + printf("."); + + printf("\n"); + } + } +}; + +#endif /* BUFFER_H */ diff --git a/bouncer/build.sh b/bouncer/build.sh new file mode 100755 index 0000000..935cae6 --- /dev/null +++ b/bouncer/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh +mkdir -p binary + +NETCODE="socketcommon.cpp client.cpp mobileclient.cpp server.cpp ircserver.cpp netcore.cpp" +SOURCES="$NETCODE main.cpp window.cpp dns.cpp" +FLAGS="-std=c++11 -DUSE_GNUTLS -lgnutls -pthread -g" + +g++ -o binary/nb4 $FLAGS $SOURCES + diff --git a/bouncer/build_static.sh b/bouncer/build_static.sh new file mode 100755 index 0000000..6ed9183 --- /dev/null +++ b/bouncer/build_static.sh @@ -0,0 +1,9 @@ +#!/bin/sh +mkdir -p binary + +NETCODE="socketcommon.cpp client.cpp mobileclient.cpp server.cpp ircserver.cpp netcore.cpp" +SOURCES="$NETCODE main.cpp window.cpp dns.cpp" +FLAGS="-static -static-libgcc -static-libstdc++ -std=c++11 -pthread" + +g++ -o binary/nb4_static $FLAGS $SOURCES + diff --git a/bouncer/client.cpp b/bouncer/client.cpp new file mode 100644 index 0000000..4fa0a12 --- /dev/null +++ b/bouncer/client.cpp @@ -0,0 +1,331 @@ +#include "core.h" + +static bool isNullSessionKey(uint8_t *key) { + for (int i = 0; i < SESSION_KEY_SIZE; i++) + if (key[i] != 0) + return false; + + return true; +} + + + +Client::Client(NetCore *_netCore) : SocketRWCommon(_netCore) { + authState = AS_LOGIN_WAIT; + memset(sessionKey, 0, sizeof(sessionKey)); + readBufPosition = 0; + + nextPacketID = 1; + lastReceivedPacketID = 0; +} +Client::~Client() { + std::list<Packet *>::iterator + i = packetCache.begin(), + e = packetCache.end(); + + for (; i != e; ++i) + delete *i; +} + + +void Client::startService(int _sock, bool withTls) { + close(); + + sock = _sock; + + if (!setSocketNonBlocking(sock)) { + perror("[Client::startService] Could not set non-blocking"); + close(); + return; + } + +#ifdef USE_GNUTLS + if (withTls) { + int initRet = gnutls_init(&tls, GNUTLS_SERVER); + if (initRet != GNUTLS_E_SUCCESS) { + printf("[Client::startService] gnutls_init borked\n"); + gnutls_perror(initRet); + close(); + return; + } + + // TODO: error check this + int ret; + const char *errPos; + + ret = gnutls_priority_set_direct(tls, "PERFORMANCE:%SERVER_PRECEDENCE", &errPos); + if (ret != GNUTLS_E_SUCCESS) { + printf("gnutls_priority_set_direct failure: %s\n", gnutls_strerror(ret)); + close(); + return; + } + + ret = gnutls_credentials_set(tls, GNUTLS_CRD_CERTIFICATE, g_clientCreds); + if (ret != GNUTLS_E_SUCCESS) { + printf("gnutls_credentials_set failure: %s\n", gnutls_strerror(ret)); + close(); + return; + } + + gnutls_certificate_server_set_request(tls, GNUTLS_CERT_IGNORE); + + gnutls_transport_set_int(tls, sock); + + tlsActive = true; + + state = CS_TLS_HANDSHAKE; + + printf("[fd=%d] preparing for TLS handshake\n", sock); + } else +#endif + { + state = CS_CONNECTED; + } +} + +void Client::close() { + SocketRWCommon::close(); + + if (authState == AS_AUTHED) + deadTime = time(NULL) + SESSION_KEEPALIVE; + else + deadTime = time(NULL) - 1; // kill instantly +} + + +void Client::generateSessionKey() { + time_t now = time(NULL); + + while (true) { + for (int i = 0; i < SESSION_KEY_SIZE; i++) { + if (i < sizeof(time_t)) + sessionKey[i] = ((uint8_t*)&now)[i]; + else + sessionKey[i] = rand() & 255; + } + + // Is any other client already using this key? + // It's ridiculously unlikely, but... probably best + // to check just in case! + bool foundMatch = false; + + for (int i = 0; i < netCore->clientCount; i++) { + if (netCore->clients[i] != this) { + if (!memcmp(netCore->clients[i]->sessionKey, sessionKey, SESSION_KEY_SIZE)) + foundMatch = true; + } + } + + // If there's none, we can safely leave! + if (!foundMatch) + break; + } +} + +void Client::clearCachedPackets(int maxID) { + packetCache.remove_if([maxID](Packet *&pkt) { + return (pkt->id <= maxID); + }); +} + + +void Client::handlePacket(Packet::Type type, char *data, int size) { + Buffer pkt; + pkt.useExistingBuffer(data, size); + + printf("[fd=%d] Packet : type %d, size %d\n", sock, type, size); + + if (authState == AS_LOGIN_WAIT) { + if (type == Packet::C2B_OOB_LOGIN) { + int error = 0; + + uint32_t protocolVersion = pkt.readU32(); + if (protocolVersion != PROTOCOL_VERSION) + error = 1; + + uint32_t lastReceivedByClient = pkt.readU32(); + + if (!pkt.readRemains(SESSION_KEY_SIZE)) + error = 2; + + // Authentication goes here at some point, too + + + if (error != 0) { + // Send an error... + Buffer pkt; + pkt.writeU32(error); + sendPacket(Packet::B2C_OOB_LOGIN_FAILED, pkt, /*allowUnauthed=*/true); + + // Would close() now but this means the login failed packet never gets sent + // need to figure out a fix for this. TODO FIXME etc etc. + + } else { + // or log us in! + uint8_t reqKey[SESSION_KEY_SIZE]; + pkt.read((char *)reqKey, SESSION_KEY_SIZE); + + printf("[fd=%d] Client authenticating\n", sock); + + if (!isNullSessionKey(reqKey)) { + printf("[fd=%d] Trying to resume session...", sock); + printf("(last they received = %d)\n", lastReceivedByClient); + + Client *other = netCore->findClientWithSessionKey(reqKey); + printf("[fd=%d] Got client %p\n", sock, other); + + if (other && other->authState == AS_AUTHED) { + printf("Valid: last packet we sent = %d\n", other->nextPacketID - 1); + // Yep, we can go! + other->resumeSession(this, lastReceivedByClient); + return; + } + } + + // If we got here, it means we couldn't resume the session. + // Start over. + printf("[fd=%d] Creating new session\n", sock); + + generateSessionKey(); + authState = AS_AUTHED; + + Buffer pkt; + pkt.append((char *)sessionKey, SESSION_KEY_SIZE); + sendPacket(Packet::B2C_OOB_LOGIN_SUCCESS, pkt); + + sessionStartEvent(); + } + + } else { + printf("[fd=%d] Unrecognised packet in AS_LOGIN_WAIT authstate: type %d, size %d\n", + sock, type, size); + } + } else if (authState == AS_AUTHED) { + packetReceivedEvent(type, pkt); + } +} + +void Client::processReadBuffer() { + // Try to process as many packets as we have in inputBuf + + // Basic header is 8 bytes + // Extended (non-OOB) header is 16 bytes + inputBuf.readSeek(0); + readBufPosition = 0; + + while (inputBuf.readRemains(8)) { + // We have 8 bytes, so we can try to read a basic header + Packet::Type type = (Packet::Type)inputBuf.readU16(); + int reserved = inputBuf.readU16(); + uint32_t packetSize = inputBuf.readU32(); + + // Do we now have the whole packet in memory...? + int extHeaderSize = (type & Packet::T_OUT_OF_BAND_FLAG) ? 0 : 8; + + if (!inputBuf.readRemains(packetSize + extHeaderSize)) + break; + + + if (!(type & Packet::T_OUT_OF_BAND_FLAG)) { + // Handle packet system things for non-OOB packets + uint32_t packetID = inputBuf.readU32(); + uint32_t lastReceivedByClient = inputBuf.readU32(); + + lastReceivedPacketID = packetID; + clearCachedPackets(lastReceivedByClient); + } + + // Yep, we can process it! + + // Save the position of the next packet + readBufPosition = inputBuf.readTell() + packetSize; + handlePacket(type, &inputBuf.data()[inputBuf.readTell()], packetSize); + + inputBuf.readSeek(readBufPosition); + } + + // If we managed to handle anything, lop it off the buffer + inputBuf.trimFromStart(readBufPosition); + readBufPosition = 0; +} + + +void Client::resumeSession(Client *other, int lastReceivedByClient) { + close(); + + inputBuf.clear(); + inputBuf.append( + &other->inputBuf.data()[other->readBufPosition], + other->inputBuf.size() - other->readBufPosition); + + // Not sure if we need to copy the outputbuf but it can't hurt + outputBuf.clear(); + outputBuf.append(other->outputBuf.data(), other->outputBuf.size()); + + sock = other->sock; + state = other->state; +#ifdef USE_GNUTLS + tls = other->tls; + tlsActive = other->tlsActive; +#endif + + other->sock = -1; + other->state = CS_DISCONNECTED; +#ifdef USE_GNUTLS + other->tls = 0; + other->tlsActive = false; +#endif + + other->close(); + + // Now send them everything we've got! + Buffer pkt; + pkt.writeU32(lastReceivedPacketID); + sendPacket(Packet::B2C_OOB_SESSION_RESUMED, pkt); + + clearCachedPackets(lastReceivedByClient); + + std::list<Packet*>::iterator + i = packetCache.begin(), + e = packetCache.end(); + + for (; i != e; ++i) + sendPacketOverWire(*i); +} + +void Client::sendPacket(Packet::Type type, const Buffer &data, bool allowUnauthed) { + Packet *packet = new Packet; + packet->type = type; + packet->data.append(data); + + if (type & Packet::T_OUT_OF_BAND_FLAG) { + packet->id = 0; + } else { + packet->id = nextPacketID; + nextPacketID++; + } + + if (state == CS_CONNECTED) + if (authState == AS_AUTHED || allowUnauthed) + sendPacketOverWire(packet); + + if (type & Packet::T_OUT_OF_BAND_FLAG) + delete packet; + else + packetCache.push_back(packet); +} + +void Client::sendPacketOverWire(const Packet *packet) { + Buffer header; + header.writeU16(packet->type); + header.writeU16(0); + header.writeU32(packet->data.size()); + + if (!(packet->type & Packet::T_OUT_OF_BAND_FLAG)) { + header.writeU32(packet->id); + header.writeU32(lastReceivedPacketID); + } + + outputBuf.append(header); + outputBuf.append(packet->data); +} diff --git a/bouncer/core.h b/bouncer/core.h new file mode 100644 index 0000000..b5d3164 --- /dev/null +++ b/bouncer/core.h @@ -0,0 +1,373 @@ +#ifndef CORE_H +#define CORE_H + +// Set in build.sh +//#define USE_GNUTLS + +#include <string.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <time.h> +#include <sys/time.h> +#include <sys/socket.h> +#include <sys/select.h> +#include <netinet/in.h> +#include <list> +#include <map> +#include <string> + +#include "buffer.h" + +#ifdef USE_GNUTLS +#include <gnutls/gnutls.h> +#endif + +#define CLIENT_LIMIT 100 +#define SERVER_LIMIT 20 + +#define SESSION_KEEPALIVE 30 + +#define SESSION_KEY_SIZE 16 + +#define PROTOCOL_VERSION 1 + +#define SERVE_VIA_TLS false + +class NetCore; +class Bouncer; +class IRCServer; + +// Need to move this somewhere more appropriate +struct UserRef { + std::string nick, ident, hostmask; + bool isSelf, isValid; + + UserRef() { isSelf = false; isValid = false; } +}; + + +class Window { +public: + NetCore *core; + + Window(NetCore *_core); + virtual ~Window() { } + + int id; + std::list<std::string> messages; + + virtual const char *getTitle() const = 0; + virtual int getType() const = 0; + virtual void syncStateForClient(Buffer &output); + virtual void handleUserInput(const char *str) { } + + void pushMessage(const char *str); +}; + +class StatusWindow : public Window { +public: + StatusWindow(IRCServer *_server); + + IRCServer *server; + + virtual const char *getTitle() const; + virtual int getType() const; + virtual void handleUserInput(const char *str); +}; + +class Channel : public Window { +public: + Channel(IRCServer *_server, const char *_name); + + IRCServer *server; + + bool inChannel; + + std::string name, topic; + std::map<std::string, uint32_t> users; + + virtual const char *getTitle() const; + virtual int getType() const; + virtual void handleUserInput(const char *str); + virtual void syncStateForClient(Buffer &output); + + void handleNameReply(const char *str); + void handleJoin(const UserRef &user); + void handlePart(const UserRef &user, const char *message); + void handleQuit(const UserRef &user, const char *message); + void handleNick(const UserRef &user, const char *newNick); + void handleMode(const UserRef &user, const char *str); + void handlePrivmsg(const UserRef &user, const char *str); + + char getEffectivePrefixChar(const char *nick) const; + + void disconnected(); +}; + + + +class SocketRWCommon { +public: + static bool setSocketNonBlocking(int fd); // Move me! + + friend class NetCore; + +protected: + NetCore *netCore; + + Buffer inputBuf, outputBuf; + + enum ConnState { + CS_DISCONNECTED = 0, + CS_WAITING_DNS = 1, // server only + CS_WAITING_CONNECT = 2, // server only + CS_TLS_HANDSHAKE = 3, + CS_CONNECTED = 4 + }; + ConnState state; + + int sock; +#ifdef USE_GNUTLS + gnutls_session_t tls; + bool tlsActive; +#endif + +public: + SocketRWCommon(NetCore *_netCore); + virtual ~SocketRWCommon(); + + virtual void close(); + +private: +#ifdef USE_GNUTLS + bool tryTLSHandshake(); + bool hasTlsPendingData() const; +#endif + + void readAction(); + void writeAction(); + + virtual void processReadBuffer() = 0; +}; + + +struct Packet { + enum Type { + T_OUT_OF_BAND_FLAG = 0x8000, + + C2B_COMMAND = 1, + B2C_STATUS = 1, + + B2C_WINDOW_ADD = 0x100, + B2C_WINDOW_REMOVE = 0x101, + B2C_WINDOW_MESSAGE = 0x102, + B2C_WINDOW_RENAME = 0x103, + + C2B_WINDOW_INPUT = 0x102, + + B2C_CHANNEL_USER_ADD = 0x120, + B2C_CHANNEL_USER_REMOVE = 0x121, + B2C_CHANNEL_USER_RENAME = 0x122, + B2C_CHANNEL_USER_MODES = 0x123, + + C2B_OOB_LOGIN = 0x8001, + + B2C_OOB_LOGIN_SUCCESS = 0x8001, + B2C_OOB_LOGIN_FAILED = 0x8002, + B2C_OOB_SESSION_RESUMED = 0x8003, + }; + + Type type; + int id; + Buffer data; +}; + +class Client : private SocketRWCommon { + friend class NetCore; +private: + enum AuthState { + AS_LOGIN_WAIT = 0, + AS_AUTHED = 1 + }; + + AuthState authState; + uint8_t sessionKey[SESSION_KEY_SIZE]; + time_t deadTime; + + std::list<Packet *> packetCache; + int nextPacketID, lastReceivedPacketID; + +public: + Client(NetCore *_netCore); + ~Client(); + + bool isAuthed() const { return (authState == AS_AUTHED); } + + void close(); + + void sendPacket(Packet::Type type, const Buffer &data, bool allowUnauthed = false); + +private: + void startService(int _sock, bool withTls); + + int readBufPosition; + void processReadBuffer(); + + void generateSessionKey(); + void resumeSession(Client *other, int lastReceivedByClient); + + void handlePacket(Packet::Type type, char *data, int size); + void sendPacketOverWire(const Packet *packet); + void clearCachedPackets(int maxID); + + // Events! + virtual void sessionStartEvent() = 0; + virtual void sessionEndEvent() = 0; + virtual void packetReceivedEvent(Packet::Type type, Buffer &pkt) = 0; +}; + +class MobileClient : public Client { +public: + Bouncer *bouncer; + + MobileClient(Bouncer *_bouncer); + +private: + virtual void sessionStartEvent(); + virtual void sessionEndEvent(); + virtual void packetReceivedEvent(Packet::Type type, Buffer &pkt); + + void handleDebugCommand(char *line, int size); +}; + +class Server : private SocketRWCommon { + friend class NetCore; + + int port; + bool useTls; + + int dnsQueryId; + +public: + Server(NetCore *_netCore); + virtual ~Server(); + +protected: + void connect(const char *hostname, int _port, bool _useTls); + +public: + void sendLine(const char *line); // protect me! + void close(); + +private: + void tryConnectPhase(); + void connectionSuccessful(); + void processReadBuffer(); + + virtual void connectedEvent() = 0; + virtual void disconnectedEvent() = 0; + virtual void lineReceivedEvent(char *line, int size) = 0; + + virtual void attachedToCore() { } +}; + +struct IRCNetworkConfig { + char hostname[512]; + char nickname[128]; + char username[128]; + char realname[128]; + char password[128]; + int port; + bool useTls; +}; + +class IRCServer : public Server { +public: + Bouncer *bouncer; + + StatusWindow status; + std::map<std::string, Channel *> channels; + + IRCNetworkConfig config; + + char currentNick[128]; + char serverPrefix[32], serverPrefixMode[32]; + std::string serverChannelModes[4]; + + uint32_t getUserFlag(char search, const char *array) const; + uint32_t getUserFlagByPrefix(char prefix) const; + uint32_t getUserFlagByMode(char mode) const; + int getChannelModeType(char mode) const; + + IRCServer(Bouncer *_bouncer); + ~IRCServer(); + + void connect(); + + // Events! +private: + virtual void connectedEvent(); + virtual void disconnectedEvent(); + virtual void lineReceivedEvent(char *line, int size); + + virtual void attachedToCore(); + + + void resetIRCState(); + void processISupport(const char *str); + + Channel *findChannel(const char *name, bool createIfNeeded); +}; + + +class NetCore { +public: + NetCore(); + + Client *clients[CLIENT_LIMIT]; + Server *servers[SERVER_LIMIT]; + int clientCount; + int serverCount; + + void sendToClients(Packet::Type type, const Buffer &data); + + std::list<Window *> windows; + int nextWindowID; + + int registerWindow(Window *window); + void deregisterWindow(Window *window); + Window *findWindow(int id) const; + + bool quitFlag; + + int execute(); + + Client *findClientWithSessionKey(uint8_t *key) const; +private: + virtual Client *constructClient() = 0; + +public: + int registerServer(Server *server); // THIS FUNCTION WILL BE PROTECTED LATER +protected: + void deregisterServer(int id); + int findServerID(Server *server) const; +}; + +class Bouncer : public NetCore { +private: + virtual Client *constructClient(); +}; + + + + +// This is ugly as crap, TODO FIXME etc etc +#ifdef USE_GNUTLS +extern gnutls_certificate_credentials_t g_serverCreds, g_clientCreds; +#endif + +#endif /* CORE_H */ diff --git a/bouncer/dns.cpp b/bouncer/dns.cpp new file mode 100644 index 0000000..056bb09 --- /dev/null +++ b/bouncer/dns.cpp @@ -0,0 +1,159 @@ +#include "dns.h" +#include <pthread.h> +#include <errno.h> +#include <string.h> +#include <stdio.h> + +#define DNS_QUERY_COUNT 20 +#define DNS_QUERY_NAME_SIZE 256 + +enum DNSQueryState { + DQS_FREE = 0, + DQS_WAITING = 1, + DQS_COMPLETE = 2, + DQS_ERROR = 3 +}; + +struct DNSQuery { + char name[DNS_QUERY_NAME_SIZE]; + in_addr result; + DNSQueryState status; + int version; +}; + +static DNSQuery dnsQueue[DNS_QUERY_COUNT]; +static pthread_t dnsThread; +static pthread_mutex_t dnsQueueMutex; +static pthread_cond_t dnsQueueCond; + +static void *dnsThreadProc(void *); + + +void DNS::start() { + pthread_mutex_init(&dnsQueueMutex, NULL); + pthread_cond_init(&dnsQueueCond, NULL); + + pthread_create(&dnsThread, NULL, &dnsThreadProc, NULL); + + for (int i = 0; i < DNS_QUERY_COUNT; i++) { + dnsQueue[i].status = DQS_FREE; + dnsQueue[i].version = 0; + } +} + +int DNS::makeQuery(const char *name) { + int id = -1; + + pthread_mutex_lock(&dnsQueueMutex); + + for (int i = 0; i < DNS_QUERY_COUNT; i++) { + if (dnsQueue[i].status == DQS_FREE) { + id = i; + break; + } + } + + if (id != -1) { + strncpy(dnsQueue[id].name, name, sizeof(dnsQueue[id].name)); + dnsQueue[id].name[sizeof(dnsQueue[id].name) - 1] = 0; + dnsQueue[id].status = DQS_WAITING; + dnsQueue[id].version++; + printf("[DNS::%d] New query: %s\n", id, dnsQueue[id].name); + } + + pthread_mutex_unlock(&dnsQueueMutex); + pthread_cond_signal(&dnsQueueCond); + + return id; +} + +void DNS::closeQuery(int id) { + if (id < 0 || id >= DNS_QUERY_COUNT) + return; + + pthread_mutex_lock(&dnsQueueMutex); + printf("[DNS::%d] Closing query\n", id); + dnsQueue[id].status = DQS_FREE; + pthread_mutex_unlock(&dnsQueueMutex); +} + +bool DNS::checkQuery(int id, in_addr *pResult, bool *pIsError) { + if (id < 0 || id >= DNS_QUERY_COUNT) + return false; + + pthread_mutex_lock(&dnsQueueMutex); + + bool finalResult = false; + if (dnsQueue[id].status == DQS_COMPLETE) { + finalResult = true; + *pIsError = false; + memcpy(pResult, &dnsQueue[id].result, sizeof(dnsQueue[id].result)); + } else if (dnsQueue[id].status == DQS_ERROR) { + finalResult = true; + *pIsError = true; + } + + pthread_mutex_unlock(&dnsQueueMutex); + + return finalResult; +} + + +void *dnsThreadProc(void *) { + pthread_mutex_lock(&dnsQueueMutex); + + for (;;) { + for (int i = 0; i < DNS_QUERY_COUNT; i++) { + if (dnsQueue[i].status == DQS_WAITING) { + char nameCopy[DNS_QUERY_NAME_SIZE]; + memcpy(nameCopy, dnsQueue[i].name, DNS_QUERY_NAME_SIZE); + + int versionCopy = dnsQueue[i].version; + + printf("[DNS::%d] Trying %s...\n", i, nameCopy); + + pthread_mutex_unlock(&dnsQueueMutex); + + addrinfo hints, *res; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED; + + int s = getaddrinfo(nameCopy, NULL, &hints, &res); + + pthread_mutex_lock(&dnsQueueMutex); + + // Before we write to the request, check that it hasn't been + // closed (and possibly replaced...!) by another thread + + if (dnsQueue[i].status == DQS_WAITING && dnsQueue[i].version == versionCopy) { + if (s == 0) { + // Only try the first one for now... + // Is this safe? Not sure. + dnsQueue[i].status = DQS_COMPLETE; + memcpy(&dnsQueue[i].result, &((sockaddr_in*)res->ai_addr)->sin_addr, sizeof(dnsQueue[i].result)); + + printf("[DNS::%d] Resolved %s to %x\n", i, dnsQueue[i].name, dnsQueue[i].result.s_addr); + } else { + dnsQueue[i].status = DQS_ERROR; + printf("[DNS::%d] Error condition: %d\n", i, s); + } + } else { + printf("[DNS::%d] Request was cancelled before getaddrinfo completed\n", i); + } + + if (s == 0) + freeaddrinfo(res); + } + } + + pthread_cond_wait(&dnsQueueCond, &dnsQueueMutex); + } + + pthread_mutex_unlock(&dnsQueueMutex); + return NULL; +} + diff --git a/bouncer/dns.h b/bouncer/dns.h new file mode 100644 index 0000000..78ec75f --- /dev/null +++ b/bouncer/dns.h @@ -0,0 +1,15 @@ +#ifndef DNS_H +#define DNS_H + +#include <sys/types.h> +#include <sys/socket.h> +#include <netdb.h> + +namespace DNS { + void start(); + int makeQuery(const char *name); + void closeQuery(int id); + bool checkQuery(int id, in_addr *pResult, bool *pIsError); +} + +#endif /* DNS_H */ diff --git a/bouncer/ircserver.cpp b/bouncer/ircserver.cpp new file mode 100644 index 0000000..31cbd6d --- /dev/null +++ b/bouncer/ircserver.cpp @@ -0,0 +1,393 @@ +#include "core.h" + +IRCServer::IRCServer(Bouncer *_bouncer) : + Server(_bouncer), + bouncer(_bouncer), + status(this) +{ +} + +IRCServer::~IRCServer() { + bouncer->deregisterWindow(&status); + + for (auto &i : channels) { + bouncer->deregisterWindow(i.second); + delete i.second; + } +} + +void IRCServer::attachedToCore() { + bouncer->registerWindow(&status); +} + +void IRCServer::connect() { + status.pushMessage("Connecting..."); + Server::connect(config.hostname, config.port, config.useTls); +} + + +void IRCServer::resetIRCState() { + strcpy(currentNick, ""); + + strcpy(serverPrefix, "@+"); + strcpy(serverPrefixMode, "ov"); +} + + +Channel *IRCServer::findChannel(const char *name, bool createIfNeeded) { + std::map<std::string, Channel *>::iterator + check = channels.find(name); + + if (check == channels.end()) { + if (createIfNeeded) { + Channel *c = new Channel(this, name); + channels[name] = c; + return c; + } else { + return 0; + } + } else { + return check->second; + } +} + + + +void IRCServer::connectedEvent() { + resetIRCState(); + + printf("[IRCServer:%p] connectedEvent\n", this); + status.pushMessage("Connected, identifying to IRC..."); + + char buf[2048]; + + if (strlen(config.password) > 0) { + sprintf(buf, "PASS %s", config.password); + sendLine(buf); + } + + sprintf(buf, "USER %s 0 * :%s\r\nNICK %s", + config.username, config.realname, config.nickname); + sendLine(buf); +} +void IRCServer::disconnectedEvent() { + printf("[IRCServer:%p] disconnectedEvent\n", this); + status.pushMessage("Disconnected."); + + for (auto &i : channels) + i.second->disconnected(); +} +void IRCServer::lineReceivedEvent(char *line, int size) { + printf("[%d] { %s }\n", size, line); + + status.pushMessage(line); + + + // Process this line...! + UserRef user; + + // Is there a prefix? + if (line[0] == ':') { + char nickBuf[512], identBuf[512], hostBuf[512]; + int nickPos = 0, identPos = 0, hostPos = 0; + int phase = 0; + + ++line; // skip colon + + while ((*line != ' ') && (*line != 0)) { + if (phase == 0) { + // Nick + if (*line == '!') + phase = 1; + else if (*line == '@') + phase = 2; + else { + if (nickPos < 511) + nickBuf[nickPos++] = *line; + } + } else if (phase == 1) { + // Ident + if (*line == '@') + phase = 2; + else { + if (identPos < 511) + identBuf[identPos++] = *line; + } + } else if (phase == 2) { + if (hostPos < 511) + hostBuf[hostPos++] = *line; + } + + ++line; + } + + if (*line == 0) { + // Invalid line. Can't parse this. + return; + } + + ++line; // skip the space + + nickBuf[nickPos] = 0; + identBuf[identPos] = 0; + hostBuf[hostPos] = 0; + + user.nick = nickBuf; + user.ident = identBuf; + user.hostmask = hostBuf; + + user.isValid = true; + user.isSelf = (strcmp(nickBuf, currentNick) == 0); + } + + // Get the command + char cmdBuf[512]; + int cmdPos = 0; + + while ((*line != ' ') && (*line != 0)) { + if (cmdPos < 511) + cmdBuf[cmdPos++] = *line; + ++line; + } + cmdBuf[cmdPos] = 0; + + if (*line == 0) { + // Invalid line. + return; + } + + ++line; // skip the space + + // Skip the : if there is one + if (*line == ':') + ++line; + + // Get the first param, or "target" in many cases + char *allParams = line; + char *paramsAfterFirst = line; + + char targetBuf[512]; + int targetPos = 0; + + while ((*paramsAfterFirst != ' ') && (*paramsAfterFirst != 0)) { + if (targetPos < 511) + targetBuf[targetPos++] = *paramsAfterFirst; + ++paramsAfterFirst; + } + + targetBuf[targetPos] = 0; + + // If we didn't reach the end of the line, skip the space + if (*paramsAfterFirst == ' ') + ++paramsAfterFirst; + + // And if the params begin with :, skip it + if (*paramsAfterFirst == ':') + ++paramsAfterFirst; + + // Now figure out what to do with this...! + + if (strcmp(cmdBuf, "PING") == 0) { + char out[512]; + snprintf(out, 512, "PONG :%s", allParams); + sendLine(out); + return; + + } else if (strcmp(cmdBuf, "JOIN") == 0) { + Channel *c = findChannel(targetBuf, true); + if (c) { + c->handleJoin(user); + return; + } + + } else if (strcmp(cmdBuf, "PART") == 0) { + Channel *c = findChannel(targetBuf, false); + if (c) { + c->handlePart(user, paramsAfterFirst);; + return; + } + + } else if (strcmp(cmdBuf, "QUIT") == 0) { + for (auto &i : channels) + i.second->handleQuit(user, allParams); + return; + + } else if (strcmp(cmdBuf, "NICK") == 0) { + if (user.isSelf) { + strncpy(currentNick, allParams, sizeof(currentNick)); + currentNick[sizeof(currentNick) - 1] = 0; + + char buf[1024]; + snprintf(buf, 1024, "You are now known as %s", currentNick); + status.pushMessage(buf); + } + + for (auto &i : channels) + i.second->handleNick(user, allParams); + return; + + } else if (strcmp(cmdBuf, "MODE") == 0) { + Channel *c = findChannel(targetBuf, false); + if (c) { + c->handleMode(user, paramsAfterFirst); + return; + } + + } else if (strcmp(cmdBuf, "PRIVMSG") == 0) { + Channel *c = findChannel(targetBuf, true); + if (c) { + c->handlePrivmsg(user, paramsAfterFirst); + return; + } + + } else if (strcmp(cmdBuf, "001") == 0) { + status.pushMessage("[debug: currentNick change detected]"); + + strncpy(currentNick, targetBuf, sizeof(currentNick)); + currentNick[sizeof(currentNick) - 1] = 0; + return; + + } else if (strcmp(cmdBuf, "005") == 0) { + processISupport(paramsAfterFirst); + return; + + } else if (strcmp(cmdBuf, "353") == 0) { + // RPL_NAMEREPLY: + // Target is always us + // Params: Channel privacy flag, channel, user list + + char *space1 = strchr(paramsAfterFirst, ' '); + if (space1) { + char *space2 = strchr(space1 + 1, ' '); + if (space2) { + char *chanName = space1 + 1; + *space2 = 0; + + char *userNames = space2 + 1; + if (*userNames == ':') + ++userNames; + + Channel *c = findChannel(chanName, false); + + if (c) { + c->handleNameReply(userNames); + return; + } + } + } + } + + status.pushMessage("!! Unhandled !!"); +} + + +void IRCServer::processISupport(const char *line) { + while (*line != 0) { + char keyBuf[512], valueBuf[512]; + int keyPos = 0, valuePos = 0; + int phase = 0; + + // This means we've reached the end + if (*line == ':') + return; + + while ((*line != 0) && (*line != ' ')) { + if (phase == 0) { + if (*line == '=') + phase = 1; + else if (keyPos < 511) + keyBuf[keyPos++] = *line; + } else { + if (valuePos < 511) + valueBuf[valuePos++] = *line; + } + + ++line; + } + + if (*line == ' ') + ++line; + + keyBuf[keyPos] = 0; + valueBuf[valuePos] = 0; + + + // Now process the thing + + if (strcmp(keyBuf, "PREFIX") == 0) { + int prefixCount = (valuePos - 2) / 2; + + if (valueBuf[0] == '(' && valueBuf[1+prefixCount] == ')') { + if (prefixCount < 32) { + strncpy(serverPrefixMode, &valueBuf[1], prefixCount); + strncpy(serverPrefix, &valueBuf[2+prefixCount], prefixCount); + + serverPrefixMode[prefixCount] = 0; + serverPrefix[prefixCount] = 0; + } + } + } else if (strcmp(keyBuf, "CHANMODES") == 0) { + char *proc = &valueBuf[0]; + + for (int index = 0; index < 4; index++) { + if (*proc == 0) + break; + + char *start = proc; + char *end = proc; + + while ((*end != ',') && (*end != 0)) + ++end; + + // If this is a zero, we can't read any more + bool endsHere = (*end == 0); + *end = 0; + + serverChannelModes[index] = start; + char moof[1000]; + sprintf(moof, "set chanmodes %d to [%s]", index, serverChannelModes[index].c_str()); + status.pushMessage(moof); + + if (endsHere) + break; + else + proc = end + 1; + } + } + } +} + + +uint32_t IRCServer::getUserFlag(char search, const char *array) const { + uint32_t flag = 1; + + // Is this character a valid prefix? + while (*array != 0) { + if (*array == search) + return flag; + + flag <<= 1; + ++array; + } +} + +uint32_t IRCServer::getUserFlagByPrefix(char prefix) const { + return getUserFlag(prefix, serverPrefix); +} +uint32_t IRCServer::getUserFlagByMode(char mode) const { + return getUserFlag(mode, serverPrefixMode); +} + +int IRCServer::getChannelModeType(char mode) const { + for (int i = 0; i < 4; i++) { + const char *modes = serverChannelModes[i].c_str(); + + while (*modes != 0) { + if (*modes == mode) + return i + 1; + ++modes; + } + } + + return 0; +} diff --git a/bouncer/main.cpp b/bouncer/main.cpp new file mode 100644 index 0000000..5330310 --- /dev/null +++ b/bouncer/main.cpp @@ -0,0 +1,61 @@ +#include "core.h" +#include "dns.h" + +#ifdef USE_GNUTLS +static gnutls_dh_params_t dh_params; +gnutls_certificate_credentials_t g_serverCreds, g_clientCreds; + +bool initTLS() { + int ret; + ret = gnutls_global_init(); + if (ret != GNUTLS_E_SUCCESS) { + printf("gnutls_global_init failure: %s\n", gnutls_strerror(ret)); + return false; + } + + unsigned int bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_LEGACY); + + ret = gnutls_dh_params_init(&dh_params); + if (ret != GNUTLS_E_SUCCESS) { + printf("dh_params_init failure: %s\n", gnutls_strerror(ret)); + return false; + } + + ret = gnutls_dh_params_generate2(dh_params, bits); + if (ret != GNUTLS_E_SUCCESS) { + printf("dh_params_generate2 failure: %s\n", gnutls_strerror(ret)); + return false; + } + + gnutls_certificate_allocate_credentials(&g_clientCreds); + ret = gnutls_certificate_set_x509_key_file(g_clientCreds, "ssl_test.crt", "ssl_test.key", GNUTLS_X509_FMT_PEM); + if (ret != GNUTLS_E_SUCCESS) { + printf("set_x509_key_file failure: %s\n", gnutls_strerror(ret)); + return false; + } + gnutls_certificate_set_dh_params(g_clientCreds, dh_params); + + gnutls_certificate_allocate_credentials(&g_serverCreds); + + return true; +} +#endif + +int main(int argc, char **argv) { +#ifdef USE_GNUTLS + if (!initTLS()) + return EXIT_FAILURE; +#endif + + DNS::start(); + + Bouncer bounce; + + int errcode = bounce.execute(); + if (errcode < 0) { + printf("(Bouncer::execute failed with %d)\n", errcode); + return EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } +}
\ No newline at end of file diff --git a/bouncer/mobileclient.cpp b/bouncer/mobileclient.cpp new file mode 100644 index 0000000..fb8b3c1 --- /dev/null +++ b/bouncer/mobileclient.cpp @@ -0,0 +1,77 @@ +#include "core.h" + +MobileClient::MobileClient(Bouncer *_bouncer) : Client(_bouncer) { + bouncer = _bouncer; +} + +void MobileClient::sessionStartEvent() { + printf("{Session started}\n"); + + Buffer syncPacket; + syncPacket.writeU32(bouncer->windows.size()); + + std::list<Window *>::iterator + i = bouncer->windows.begin(), + e = bouncer->windows.end(); + + for (; i != e; ++i) + (*i)->syncStateForClient(syncPacket); + + sendPacket(Packet::B2C_WINDOW_ADD, syncPacket); +} +void MobileClient::sessionEndEvent() { + printf("{Session ended}\n"); +} +void MobileClient::packetReceivedEvent(Packet::Type type, Buffer &pkt) { + if (type == Packet::C2B_COMMAND) { + char cmd[2048]; + pkt.readStr(cmd, sizeof(cmd)); + handleDebugCommand(cmd, strlen(cmd)); + + } else if (type == Packet::C2B_WINDOW_INPUT) { + int winID = pkt.readU32(); + Window *window = bouncer->findWindow(winID); + if (!window) { + printf("[MobileClient:%p] Message for unknown window %d\n", this, winID); + return; + } + + char text[8192]; + pkt.readStr(text, sizeof(text)); + + window->handleUserInput(text); + + } else { + printf("[MobileClient:%p] Unrecognised packet for MobileClient: type %d, size %d\n", + this, type, pkt.size()); + } +} + +void MobileClient::handleDebugCommand(char *line, int size) { + // This is a terrible mess that will be replaced shortly + if (strncmp(line, "all ", 4) == 0) { + Buffer pkt; + pkt.writeStr(&line[4]); + for (int i = 0; i < bouncer->clientCount; i++) + bouncer->clients[i]->sendPacket(Packet::B2C_STATUS, pkt); + + } else if (strcmp(line, "quit") == 0) { + bouncer->quitFlag = true; + } else if (strncmp(&line[1], "ddsrv ", 6) == 0) { + IRCServer *srv = new IRCServer(bouncer); + strcpy(srv->config.hostname, &line[7]); + srv->config.useTls = (line[0] == 's'); + srv->config.port = (line[0] == 's') ? 1191 : 6667; + strcpy(srv->config.nickname, "Ninjifox"); + strcpy(srv->config.username, "boop"); + strcpy(srv->config.realname, "boop"); + strcpy(srv->config.password, ""); + bouncer->registerServer(srv); + + Buffer pkt; + pkt.writeStr("Your wish is my command!"); + for (int i = 0; i < bouncer->clientCount; i++) + bouncer->clients[i]->sendPacket(Packet::B2C_STATUS, pkt); + } +} + diff --git a/bouncer/netcore.cpp b/bouncer/netcore.cpp new file mode 100644 index 0000000..dee6ef7 --- /dev/null +++ b/bouncer/netcore.cpp @@ -0,0 +1,330 @@ +#include "core.h" + + +NetCore::NetCore() { + clientCount = 0; + for (int i = 0; i < CLIENT_LIMIT; i++) + clients[i] = NULL; + serverCount = 0; + for (int i = 0; i < SERVER_LIMIT; i++) + servers[i] = NULL; + + nextWindowID = 1; +} + +Client *NetCore::findClientWithSessionKey(uint8_t *key) const { + for (int i = 0; i < clientCount; i++) + if (!memcmp(clients[i]->sessionKey, key, SESSION_KEY_SIZE)) + return clients[i]; + + return 0; +} + +int NetCore::registerServer(Server *server) { + if (serverCount >= SERVER_LIMIT) + return -1; + + int id = serverCount++; + servers[id] = server; + server->attachedToCore(); + return id; +} +void NetCore::deregisterServer(int id) { + Server *server = servers[id]; + server->close(); + delete server; + + serverCount--; + servers[id] = servers[serverCount]; +} +int NetCore::findServerID(Server *server) const { + for (int i = 0; i < SERVER_LIMIT; i++) + if (servers[i] == server) + return i; + return -1; +} + +int NetCore::execute() { + // prepare the listen socket + int listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == -1) { + perror("Could not create the listener socket"); + return -1; + } + + int v = 1; + if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &v, sizeof(v)) == -1) { + perror("Could not set SO_REUSEADDR"); + return -2; + } + + sockaddr_in listenAddr; + listenAddr.sin_family = AF_INET; + listenAddr.sin_port = htons(5454); + listenAddr.sin_addr.s_addr = htonl(INADDR_ANY); + + if (bind(listener, (sockaddr *)&listenAddr, sizeof(listenAddr)) == -1) { + perror("Could not bind to the listener socket"); + return -3; + } + + if (!SocketRWCommon::setSocketNonBlocking(listener)) { + perror("[Listener] Could not set non-blocking"); + return -4; + } + + if (listen(listener, 10) == -1) { + perror("Could not listen()"); + return -5; + } + + printf("Listening!\n"); + + + // do stuff! + while (!quitFlag) { + fd_set readSet, writeSet; + FD_ZERO(&readSet); + FD_ZERO(&writeSet); + + int maxFD = listener; + FD_SET(listener, &readSet); + + time_t now = time(NULL); + + for (int i = 0; i < clientCount; i++) { +#ifdef USE_GNUTLS + if (clients[i]->state == Client::CS_TLS_HANDSHAKE) + clients[i]->tryTLSHandshake(); +#endif + + if (clients[i]->sock != -1) { + if (clients[i]->sock > maxFD) + maxFD = clients[i]->sock; + + if (clients[i]->state == Client::CS_CONNECTED) + FD_SET(clients[i]->sock, &readSet); + if (clients[i]->outputBuf.size() > 0) + FD_SET(clients[i]->sock, &writeSet); + + } else { + // Outdated session, can we kill it? + if (now >= clients[i]->deadTime) { + printf("[%d] Session expired, deleting\n", now); + + // Yep. + Client *client = clients[i]; + if (client->authState == Client::AS_AUTHED) + client->sessionEndEvent(); + delete client; + + // If this is the last socket in the list, we can just + // decrement clientCount and all will be fine. + clientCount--; + + // Otherwise, we move that pointer into this slot, and + // we subtract one from i so that we'll process that slot + // on the next loop iteration. + if (i != clientCount) { + clients[i] = clients[clientCount]; + i--; + } + } + } + } + + for (int i = 0; i < serverCount; i++) { + if (servers[i]->state == Server::CS_WAITING_DNS) + servers[i]->tryConnectPhase(); +#ifdef USE_GNUTLS + else if (servers[i]->state == Server::CS_TLS_HANDSHAKE) { + if (servers[i]->tryTLSHandshake()) + servers[i]->connectedEvent(); + } +#endif + + if (servers[i]->sock != -1) { + if (servers[i]->sock > maxFD) + maxFD = servers[i]->sock; + + if (servers[i]->state == Server::CS_CONNECTED) + FD_SET(servers[i]->sock, &readSet); + if (servers[i]->outputBuf.size() > 0 || servers[i]->state == Server::CS_WAITING_CONNECT) + FD_SET(servers[i]->sock, &writeSet); + } + } + + timeval timeout; + timeout.tv_sec = 1; + timeout.tv_usec = 0; + int numFDs = select(maxFD+1, &readSet, &writeSet, NULL, &timeout); + + now = time(NULL); + //printf("[%lu select:%d]\n", now, numFDs); + + + for (int i = 0; i < clientCount; i++) { + if (clients[i]->sock != -1) { + if (FD_ISSET(clients[i]->sock, &writeSet)) + clients[i]->writeAction(); + + if (FD_ISSET(clients[i]->sock, &readSet) +#ifdef USE_GNUTLS + || clients[i]->hasTlsPendingData() +#endif + ) + { + clients[i]->readAction(); + } + } + } + + for (int i = 0; i < serverCount; i++) { + if (servers[i]->sock != -1) { + if (FD_ISSET(servers[i]->sock, &writeSet)) { + Server *server = servers[i]; + + if (server->state == Server::CS_WAITING_CONNECT) { + // Welp, this means we're connected! + // Maybe. + // We might have an error condition, in which case, + // we're screwed. + bool didSucceed = false; + int sockErr; + socklen_t sockErrSize = sizeof(sockErr); + + if (getsockopt(server->sock, SOL_SOCKET, SO_ERROR, &sockErr, &sockErrSize) == 0) { + if (sockErr == 0) + didSucceed = true; + } + + if (didSucceed) { + // WE'RE IN fuck yeah + printf("[%d] Connection succeeded!\n", i); + server->connectionSuccessful(); + } else { + // Nope. Nuke it. + printf("[%d] Connection failed: %d\n", i, sockErr); + server->close(); + } + + } else { + server->writeAction(); + } + } + + + if (FD_ISSET(servers[i]->sock, &readSet) +#ifdef USE_GNUTLS + || servers[i]->hasTlsPendingData() +#endif + ) + { + servers[i]->readAction(); + } + } + } + + + + if (FD_ISSET(listener, &readSet)) { + // Yay, we have a new connection + int sock = accept(listener, NULL, NULL); + + if (clientCount >= CLIENT_LIMIT) { + // We can't accept it. + printf("Too many connections, we can't accept this one. THIS SHOULD NEVER HAPPEN.\n"); + shutdown(sock, SHUT_RDWR); + close(sock); + } else { + // Create a new connection + printf("[%d] New connection, fd=%d\n", clientCount, sock); + + Client *client = constructClient(); + + clients[clientCount] = client; + ++clientCount; + + client->startService(sock, SERVE_VIA_TLS); + } + } + } + + // Need to shut down all sockets here + for (int i = 0; i < serverCount; i++) + servers[i]->close(); + + for (int i = 0; i < clientCount; i++) + clients[i]->close(); + + shutdown(listener, SHUT_RDWR); + close(listener); + + for (int i = 0; i < serverCount; i++) + delete servers[i]; + for (int i = 0; i < clientCount; i++) + delete clients[i]; + + serverCount = clientCount = 0; + + return 0; +} + + + +int NetCore::registerWindow(Window *window) { + window->id = nextWindowID; + nextWindowID++; + + windows.push_back(window); + + + Buffer pkt; + pkt.writeU32(1); + window->syncStateForClient(pkt); + + for (int i = 0; i < clientCount; i++) + if (clients[i]->isAuthed()) + clients[i]->sendPacket(Packet::B2C_WINDOW_ADD, pkt); +} + +void NetCore::deregisterWindow(Window *window) { + Buffer pkt; + pkt.writeU32(1); + pkt.writeU32(window->id); + + for (int i = 0; i < clientCount; i++) + if (clients[i]->isAuthed()) + clients[i]->sendPacket(Packet::B2C_WINDOW_REMOVE, pkt); + + windows.remove(window); +} + +Window *NetCore::findWindow(int id) const { + std::list<Window *>::const_iterator + i = windows.begin(), + e = windows.end(); + + for (; i != e; ++i) + if ((*i)->id == id) + return *i; + + return 0; +} + + +void NetCore::sendToClients(Packet::Type type, const Buffer &data) { + for (int i = 0; i < clientCount; i++) + if (clients[i]->isAuthed()) + clients[i]->sendPacket(type, data); +} + + + +Client *Bouncer::constructClient() { + return new MobileClient(this); +} + + + + diff --git a/bouncer/server.cpp b/bouncer/server.cpp new file mode 100644 index 0000000..16c754b --- /dev/null +++ b/bouncer/server.cpp @@ -0,0 +1,160 @@ +#include "core.h" +#include "dns.h" + +Server::Server(NetCore *_netCore) : SocketRWCommon(_netCore) { + dnsQueryId = -1; +} +Server::~Server() { + if (dnsQueryId != -1) + DNS::closeQuery(dnsQueryId); + close(); +} + + + +void Server::processReadBuffer() { + // Try to process as many lines as we can + char *buf = inputBuf.data(); + int bufSize = inputBuf.size(); + int lineBegin = 0, pos = 0; + + while (pos < bufSize) { + if (buf[pos] == '\r' || buf[pos] == '\n') { + if (pos > lineBegin) { + buf[pos] = 0; + lineReceivedEvent(&buf[lineBegin], pos - lineBegin); + } + + lineBegin = pos + 1; + } + + pos++; + } + + // If we managed to handle anything, lop it off the buffer + inputBuf.trimFromStart(lineBegin); +} + +void Server::sendLine(const char *line) { + outputBuf.append(line, strlen(line)); + outputBuf.append("\r\n", 2); +} + + +void Server::connect(const char *hostname, int _port, bool _useTls) { + if (state == CS_DISCONNECTED) { + port = _port; + useTls = _useTls; + + DNS::closeQuery(dnsQueryId); // just in case + dnsQueryId = DNS::makeQuery(hostname); + + if (dnsQueryId == -1) { + // TODO: better error reporting + printf("DNS query failed!\n"); + } else { + state = CS_WAITING_DNS; + } + } +} + +void Server::tryConnectPhase() { + if (state == CS_WAITING_DNS) { + in_addr result; + bool isError; + + if (DNS::checkQuery(dnsQueryId, &result, &isError)) { + DNS::closeQuery(dnsQueryId); + dnsQueryId = -1; + + if (isError) { + printf("DNS query failed at phase 2!\n"); + state = CS_DISCONNECTED; + } else { + // OK, if there was no error, we can go ahead and do this... + + sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == -1) { + perror("[Server] Failed to socket()"); + close(); + return; + } + + if (!setSocketNonBlocking(sock)) { + perror("[Server] Could not set non-blocking"); + close(); + return; + } + + // We have our non-blocking socket, let's try connecting! + sockaddr_in outAddr; + outAddr.sin_family = AF_INET; + outAddr.sin_port = htons(port); + outAddr.sin_addr.s_addr = result.s_addr; + + if (::connect(sock, (sockaddr *)&outAddr, sizeof(outAddr)) == -1) { + if (errno == EINPROGRESS) { + state = CS_WAITING_CONNECT; + } else { + perror("[Server] Could not connect"); + close(); + } + } else { + // Whoa, we're connected? Neat. + connectionSuccessful(); + } + } + } + } +} + +void Server::connectionSuccessful() { + state = CS_CONNECTED; + + inputBuf.clear(); + outputBuf.clear(); + + // Do we need to do any TLS junk? +#ifdef USE_GNUTLS + if (useTls) { + state = CS_TLS_HANDSHAKE; + + int initRet = gnutls_init(&tls, GNUTLS_CLIENT); + if (initRet != GNUTLS_E_SUCCESS) { + printf("[Server::connectionSuccessful] gnutls_init borked\n"); + gnutls_perror(initRet); + close(); + return; + } + + // TODO: error check this + const char *errPos; + gnutls_priority_set_direct(tls, "NORMAL", &errPos); + + gnutls_credentials_set(tls, GNUTLS_CRD_CERTIFICATE, g_serverCreds); + + gnutls_transport_set_int(tls, sock); + + tlsActive = true; + } else +#endif + { + connectedEvent(); + } +} + +void Server::close() { + int saveState = state; + + SocketRWCommon::close(); + + if (dnsQueryId != -1) { + DNS::closeQuery(dnsQueryId); + dnsQueryId = -1; + } + + if (saveState == CS_CONNECTED) + disconnectedEvent(); +} + + diff --git a/bouncer/socketcommon.cpp b/bouncer/socketcommon.cpp new file mode 100644 index 0000000..7bc55b6 --- /dev/null +++ b/bouncer/socketcommon.cpp @@ -0,0 +1,174 @@ +#include "core.h" + +/*static*/ bool SocketRWCommon::setSocketNonBlocking(int sock) { + int opts = fcntl(sock, F_GETFL); + if (opts < 0) { + perror("Could not get fcntl options\n"); + return false; + } + opts |= O_NONBLOCK; + if (fcntl(sock, F_SETFL, opts) == -1) { + perror("Could not set fcntl options\n"); + return false; + } + return true; +} + + +SocketRWCommon::SocketRWCommon(NetCore *_netCore) { + netCore = _netCore; + sock = -1; + state = CS_DISCONNECTED; +#ifdef USE_GNUTLS + tlsActive = false; +#endif +} +SocketRWCommon::~SocketRWCommon() { + close(); +} + +#ifdef USE_GNUTLS +bool SocketRWCommon::hasTlsPendingData() const { + if (tlsActive) + return (gnutls_record_check_pending(tls) > 0); + else + return false; +} + +bool SocketRWCommon::tryTLSHandshake() { + int hsRet = gnutls_handshake(tls); + if (gnutls_error_is_fatal(hsRet)) { + printf("[SocketRWCommon::tryTLSHandshake] gnutls_handshake borked\n"); + gnutls_perror(hsRet); + close(); + return false; + } + + if (hsRet == GNUTLS_E_SUCCESS) { + // We're in !! + state = CS_CONNECTED; + + inputBuf.clear(); + outputBuf.clear(); + + printf("[SocketRWCommon connected via SSL!]\n"); + return true; + } + + return false; +} +#endif + +void SocketRWCommon::close() { + if (sock != -1) { +#ifdef USE_GNUTLS + if (tlsActive) + gnutls_bye(tls, GNUTLS_SHUT_RDWR); +#endif + shutdown(sock, SHUT_RDWR); + ::close(sock); + } + + sock = -1; + inputBuf.clear(); + outputBuf.clear(); + state = CS_DISCONNECTED; + +#ifdef USE_GNUTLS + if (tlsActive) { + gnutls_deinit(tls); + tlsActive = false; + } +#endif +} + +void SocketRWCommon::readAction() { + // Ensure we have at least 0x200 bytes space free + // (Up this, maybe?) + int bufSize = inputBuf.size(); + int requiredSize = bufSize + 0x200; + if (requiredSize > inputBuf.capacity()) + inputBuf.setCapacity(requiredSize); + + ssize_t amount; + +#ifdef USE_GNUTLS + if (tlsActive) { + amount = gnutls_record_recv(tls, + &inputBuf.data()[bufSize], + 0x200); + } else +#endif + { + amount = recv(sock, + &inputBuf.data()[bufSize], + 0x200, + 0); + } + + + if (amount > 0) { + // Yep, we have data + printf("[fd=%d] Read %d bytes\n", sock, amount); + inputBuf.resize(bufSize + amount); + + processReadBuffer(); + + } else if (amount == 0) { + printf("[fd=%d] Read 0! Socket closing.\n", sock); + close(); + + } else if (amount < 0) { +#ifdef USE_GNUTLS + if (tlsActive) { + if (gnutls_error_is_fatal(amount)) { + printf("Error while reading [gnutls %d]!\n", amount); + close(); + } + } else +#endif + { + perror("Error while reading!"); + close(); + } + } +} + +void SocketRWCommon::writeAction() { + // What can we get rid of...? + ssize_t amount; + +#ifdef USE_GNUTLS + if (tlsActive) { + amount = gnutls_record_send(tls, + outputBuf.data(), + outputBuf.size()); + } else +#endif + { + amount = send(sock, + outputBuf.data(), + outputBuf.size(), + 0); + } + + if (amount > 0) { + printf("[fd=%d] Wrote %d bytes out of %d\n", sock, amount, outputBuf.size()); + outputBuf.trimFromStart(amount); + } else if (amount == 0) + printf("Sent 0!\n"); + else if (amount < 0) { +#ifdef USE_GNUTLS + if (tlsActive) { + if (gnutls_error_is_fatal(amount)) { + printf("Error while sending [gnutls %d]!\n", amount); + close(); + } + } else +#endif + { + perror("Error while sending!"); + close(); + } + } +} diff --git a/bouncer/window.cpp b/bouncer/window.cpp new file mode 100644 index 0000000..c971046 --- /dev/null +++ b/bouncer/window.cpp @@ -0,0 +1,468 @@ +#include "core.h" + +Window::Window(NetCore *_core) { + core = _core; +} + +void Window::syncStateForClient(Buffer &output) { + output.writeU32(getType()); + output.writeU32(id); + output.writeStr(getTitle()); + + output.writeU32(messages.size()); + + std::list<std::string>::iterator + i = messages.begin(), + e = messages.end(); + + for (; i != e; ++i) { + output.writeStr(i->c_str()); + } +} + +void Window::pushMessage(const char *str) { + messages.push_back(str); + + bool createdPacket = false; + Buffer packet; + + for (int i = 0; i < core->clientCount; i++) { + if (core->clients[i]->isAuthed()) { + if (!createdPacket) { + packet.writeU32(id); + packet.writeStr(str); + createdPacket = true; + } + + core->clients[i]->sendPacket(Packet::B2C_WINDOW_MESSAGE, packet); + } + } +} + + + + +StatusWindow::StatusWindow(IRCServer *_server) : + Window(_server->bouncer), + server(_server) +{ +} + +const char *StatusWindow::getTitle() const { + return server->config.hostname; +} + +int StatusWindow::getType() const { + return 1; +} + +void StatusWindow::handleUserInput(const char *str) { + if (str[0] == '/') { + // moof + if (strcmp(str, "/connect") == 0) { + server->connect(); + } else if (strcmp(str, "/disconnect") == 0) { + server->close(); + } else if (strncmp(str, "/password ", 10) == 0) { + pushMessage("Password set."); + + // This is ugly, ugh + strncpy( + server->config.password, + &str[10], + sizeof(server->config.password)); + server->config.password[sizeof(server->config.password) - 1] = 0; + } + } else { + server->sendLine(str); + } +} + + + + +Channel::Channel(IRCServer *_server, const char *_name) : + Window(_server->bouncer), + server(_server), + inChannel(false), + name(_name) +{ + server->bouncer->registerWindow(this); +} + +const char *Channel::getTitle() const { + return name.c_str(); +} + +int Channel::getType() const { + return 2; +} + +void Channel::handleUserInput(const char *str) { + char msgBuf[16384]; + + if (str[0] == '/') { + if (strncmp(str, "/me ", 4) == 0) { + // The duplication of code between here and + // handlePrivmsg is ugly. TODO: fixme. + char prefix[2]; + prefix[0] = getEffectivePrefixChar(server->currentNick); + prefix[1] = 0; + + snprintf(msgBuf, sizeof(msgBuf), + "* %s%s %s", + prefix, + server->currentNick, + &str[4]); + pushMessage(msgBuf); + + snprintf(msgBuf, sizeof(msgBuf), + "PRIVMSG %s :\x01" "ACTION %s\x01", + name.c_str(), + &str[4]); + server->sendLine(msgBuf); + } + } else { + // Aaaand this is also pretty ugly ><;; + // TODO: fixme. + char prefix[2]; + prefix[0] = getEffectivePrefixChar(server->currentNick); + prefix[1] = 0; + + snprintf(msgBuf, sizeof(msgBuf), + "<%s%s> %s", + prefix, + server->currentNick, + str); + pushMessage(msgBuf); + + snprintf(msgBuf, sizeof(msgBuf), + "PRIVMSG %s :%s", + name.c_str(), + str); + server->sendLine(msgBuf); + } +} + +void Channel::syncStateForClient(Buffer &output) { + Window::syncStateForClient(output); + + output.writeU32(users.size()); + + for (auto &i : users) { + output.writeStr(i.first.c_str()); + output.writeU32(i.second); + } + + output.writeStr(topic.c_str()); +} + + +void Channel::handleNameReply(const char *str) { + char copy[4096]; + strncpy(copy, str, 4096); + copy[4095] = 0; + + char *strtok_var; + char *name = strtok_r(copy, " ", &strtok_var); + + int nameCount = 0; + + Buffer packet; + packet.writeU32(id); + packet.writeU32(0); // Dummy value..! + + while (name) { + uint32_t modes = 0; + + // Check the beginning of the name for as many valid + // mode prefixes as possible + // Servers may only send one, but I want to take into + // account the possibility that they might send multiple + // ones. Just in case. + + while (*name != 0) { + uint32_t flag = server->getUserFlagByPrefix(*name); + + if (flag == 0) + break; + else + modes |= flag; + + ++name; + } + + // Got it! + users[name] = modes; + + nameCount++; + //packet.writeU8(getEffectivePrefixChar(name)); + packet.writeStr(name); + packet.writeU32(modes); + + // Get the next name + name = strtok_r(NULL, " ", &strtok_var); + } + + if (nameCount > 0) { + uint32_t nameCountU32 = nameCount; + memcpy(&packet.data()[4], &nameCountU32, sizeof(uint32_t)); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_ADD, packet); + } +} + +void Channel::handleJoin(const UserRef &user) { + if (user.isSelf) { + Buffer packet; + packet.writeU32(id); + packet.writeU32(0); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_REMOVE, packet); + + + users.clear(); + + inChannel = true; + pushMessage("You have joined the channel!"); + } else { + Buffer packet; + packet.writeU32(id); + packet.writeU32(1); + packet.writeStr(user.nick.c_str()); + packet.writeU32(0); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_ADD, packet); + + users[user.nick] = 0; + + char buf[1024]; + snprintf(buf, 1024, + "%s (%s@%s) has joined", + user.nick.c_str(), + user.ident.c_str(), + user.hostmask.c_str()); + + pushMessage(buf); + } +} + +void Channel::handlePart(const UserRef &user, const char *message) { + auto i = users.find(user.nick); + if (i != users.end()) { + users.erase(i); + + Buffer packet; + packet.writeU32(id); + packet.writeU32(1); + packet.writeStr(user.nick.c_str()); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_REMOVE, packet); + } + + char buf[1024]; + + if (user.isSelf) { + inChannel = false; + + snprintf(buf, 1024, + "You have left the channel (%s)", + message); + pushMessage(buf); + } else { + snprintf(buf, 1024, + "%s (%s@%s) has parted (%s)", + user.nick.c_str(), + user.ident.c_str(), + user.hostmask.c_str(), + message); + + pushMessage(buf); + } +} + +void Channel::handleQuit(const UserRef &user, const char *message) { + if (user.isSelf) + inChannel = false; + + auto i = users.find(user.nick); + if (i == users.end()) + return; + + users.erase(i); + + Buffer packet; + packet.writeU32(id); + packet.writeU32(1); + packet.writeStr(user.nick.c_str()); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_REMOVE, packet); + + char buf[1024]; + + snprintf(buf, 1024, + "%s (%s@%s) has quit (%s)", + user.nick.c_str(), + user.ident.c_str(), + user.hostmask.c_str(), + message); + + pushMessage(buf); +} + +void Channel::handleNick(const UserRef &user, const char *newNick) { + auto i = users.find(user.nick); + if (i == users.end()) + return; + + users[newNick] = i->second; + users.erase(i); + + Buffer packet; + packet.writeU32(id); + packet.writeStr(user.nick.c_str()); + packet.writeStr(newNick); + + server->bouncer->sendToClients( + Packet::B2C_CHANNEL_USER_RENAME, packet); + + char buf[1024]; + snprintf(buf, 1024, + "%s is now known as %s", + user.nick.c_str(), + newNick); + + pushMessage(buf); +} + +void Channel::handleMode(const UserRef &user, const char *str) { + char copy[4096]; + strncpy(copy, str, 4096); + copy[4095] = 0; + + char *strtok_var; + char *modes = strtok_r(copy, " ", &strtok_var); + + if (!modes) + return; + + bool addFlag = true; + + while (*modes != 0) { + char mode = *(modes++); + + uint32_t flag; + + if (mode == '+') { + addFlag = true; + } else if (mode == '-') { + addFlag = false; + + } else if ((flag = server->getUserFlagByMode(mode)) != 0) { + bool oops = false; + char *target = strtok_r(NULL, " ", &strtok_var); + + auto i = users.find(target); + if (i == users.end()) { + // Oops? Spit out an error... + oops = true; + } else { + // TODO: push mode change to clients + uint32_t flags = i->second; + if (addFlag) + flags |= flag; + else + flags &= ~flag; + users[target] = flags; + } + + char buf[1024]; + snprintf(buf, 1024, + "%s %s mode %c on %s%s", + user.nick.c_str(), + addFlag ? "set" : "cleared", + mode, + target, + oops ? ", but something went wrong!" : ""); + pushMessage(buf); + + } else { + int type = server->getChannelModeType(mode); + char *param = 0; + + switch (type) { + case 1: + case 2: + // Always get a parameter + param = strtok_r(NULL, " ", &strtok_var); + break; + case 3: + // Only get a parameter if adding + if (addFlag) + param = strtok_r(NULL, " ", &strtok_var); + break; + } + + char buf[1024]; + snprintf(buf, 1024, + "%s %s channel mode %c%s%s", + user.nick.c_str(), + addFlag ? "set" : "cleared", + mode, + param ? " " : "", + param ? param : ""); + pushMessage(buf); + } + } +} + +void Channel::handlePrivmsg(const UserRef &user, const char *str) { + char prefix[2]; + prefix[0] = getEffectivePrefixChar(user.nick.c_str()); + prefix[1] = 0; + + char buf[15000]; + snprintf(buf, 15000, + "<%s%s> %s", + prefix, + user.nick.c_str(), + str); + + pushMessage(buf); +} + + +char Channel::getEffectivePrefixChar(const char *nick) const { + auto i = users.find(nick); + if (i == users.end()) + return 0; + + // Maybe this bit would work best as an IRCServer method? + + uint32_t modes = i->second; + uint32_t flag = 1; + char *prefixes = server->serverPrefix; + + while (*prefixes != 0) { + if (modes & flag) + return *prefixes; + + ++prefixes; + flag <<= 1; + } + + return 0; +} + + +void Channel::disconnected() { + if (inChannel) { + inChannel = false; + pushMessage("You have been disconnected."); + } +} |