Portál AbcLinuxu, 7. května 2024 20:58
V tomto díle si ukážeme, jak za použití modulu QtNetwork naprogramovat jednoduchý TCP server.
Tento díl seriálu úzce souvisí s článkem Grafické programy v Qt 4 – 8 (TCP klient), jelikož je o server, jehož klienta jsem napsal s grafickým rozhraním. Základní informace o TCP síťování, socketech v Qt a ukázkovém programu se dočtete tam, zatímco tento článek popisuje čistě serverovou část, takže doporučuji začít čtení odkazovaným článkem.
Teď, když chápeme komunikaci přes QTcpSocket
, můžeme se vrhnout na serverovou část. Základem je třída QTcpServer
. Hned po vytvoření instance je vhodné nastavit nějaké základní parametry serveru, například takto:
QTcpServer* server = new QTcpServer(this); // explicitně zakážeme použití proxy server->setProxy(QNetworkProxy::NoProxy); // nastavíme max. počet spojení server->setMaxPendingConnections(50);
Ve většině případů je dobré zpracovat signál newConnection()
, kterým nás server informuje o novém příchozím spojení. Slot, který na tento signál napojíme, bude chtít nejspíš získat socket tohoto nového spojení. K tomu využijeme metodu nextPendingConnection()
, a to následovně:
// získáme socket QTcpSocket* socket = server->nextPendingConnection(); // dobré je hned ověřit, zda ještě existuje (tj. zda metoda nevrátila 0) if(!socket) return;
Existují dvě alternativy k tomuto přístupu zpracování nových příchozích spojení a jejich použití záleží na tom, čeho přesně chcete dosáhnout. Pokud potřebujete provádět něco velmi specifického a vyžadujete vysokou flexibilitu, potom si celou událost můžete zpracovat sami, a to tak, že si vytvoříte vlastní třídu založenou na QTcpServer
a reimplementujete její chráněnou virtuální metodu incomingConnection(int socketDescriptor)
. Integer, který dostanete jako argument, je popisovač (descriptor) socketu a pokud nepoužíváte QNetworkProxy
, lze s ním pracovat pomocí nativních socketových funkcí.
Druhý alternativní způsob je podstatně jednodušší. Nejdřív je třeba spustit server (viz níže) a potom lze použít blokující metodu waitForNewConnection()
, které lze zadat čas v milisekundách (jak dlouho má čekat na příchozí připojení) a ukazatel na bool
, do kterého se uloží informace o tom, zda došlo k vypršení času (tedy false
znamená, že máte nové spojení).
Když máme připravené zpracování nových připojení, můžeme spustit server, tedy naslouchání na určité adrese (nebo více adresách) a portu. Třeba takto:
QString ip("127.0.0.1"); QHostAddress addr; if(!addr.setAddress(ip)) { // zadaná IP adresa není ve správném formátu return 1; } // port=0 zajistí automatický výběr portu server->listen(addr, 0);
Ještě než server spustíte, je třeba vytvořit mu konfigurační soubor, kam zapíšete platná uživatelská jména (bez mezer) a přiřadíte jim hesla (resp. jejich SHA1 hash). Chcete-li vytvořit uživatele user s heslem pass, tak si nejdřív zjistěte SHA1 hash řetězce „pass“:
$ echo -n "pass" | sha1sum 9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
Když máte hash, vytvořte si konfigurační soubor v textovém editoru (na UNIXech ~/.config/watzke.cz/todoserver.conf
) a přidejte do něj sekci [logins]
a vaším uživatelem.
[logins] user=9d4e1e23bd5b727046a9e3b4b7db57bd8d6ee684
Server komunikuje jednoduchým protokolem, který je zdokumentovaný ve výše odkazovaném článku. Obsahuje jednoduchou obranu před bruteforce přihlašováním – povolí maximálně 10 chybných přihlášení za minutu. Jde spíše jen o ukázku implementace takové věci, spíš než cokoliv jiného, nicméně crackera by to mělo zpomalit :-)
Samozřejmě teď lze namítnout, že zmiňovat crackery je výsměch, když nepoužívám SSL sockety (nebo jiný způsob šifrování), ale o to mi v této ukázce nejde. Nicméně, když najdete bezpečnostní chybu, která s tímto nesouvisí, určitě se ozvěte v diskusi. Ještě dodám, že o chybějícím limitu délky jednotlivých záznamů vím, může tam být (teoreticky) libovolně dlouhý řetězec, i když můj klient umožní max. 32767 (2^15-1) znaků. Sám jsem několik bezpečnostních chyb odhalil a opravil při testování. Zvlášť při psaní síťového serveru, kde se řeší přihlašování, je třeba korektně ošetřovat nejrůznější výjimky. Nikdy nespoléhejte pouze na kontroly ve vašem klientovi.
tcpserver.h
: API.
#ifndef TCPSERVER_H #define TCPSERVER_H #include <QObject> #include <QTextStream> #include <QList> #include <QPair> #include <QHostAddress> #include <QHash> #include <QSettings> class QTcpServer; class QTcpSocket; class TcpServer : public QObject { Q_OBJECT public: TcpServer(QHostAddress addr = QHostAddress(QHostAddress::Any), int port = 2266); private: QTcpServer* server; QTextStream cerr; QHash<QTcpSocket*, QString> logins; QHash<QString, int> log; QSettings settings; void login(QTcpSocket* socket, const QString& user, const QString& pass, bool force = false); QString getKey(QString user, int index); void sortKeys(QString user); void listItems(QTcpSocket* socket); void addItem(QTcpSocket* socket, QString item); void removeItems(QTcpSocket* socket, QList<QByteArray> keys); void socketWrite(QTcpSocket* socket, QByteArray data); private slots: void purgeLog(); void handleNewConnection(); void reply(); void gotDisconnected(); }; #endif // TCPSERVER_H
tcpserver.cpp
:
#include "tcpserver.h" #include <QtNetwork> #include <cstdio> TcpServer::TcpServer(QHostAddress addr, int port) : cerr(stderr, QIODevice::WriteOnly) // poslouží pro výpis na std. chybový výstup, // podobně jako std::cerr v C++ { // vytvoříme TCP server server = new QTcpServer(this); // zajistíme zpracování požadavků vlastním slotem connect(server, SIGNAL(newConnection()), this, SLOT(handleNewConnection())); // každou minutu pročistíme záznam přihlašování QTimer::singleShot(60*1000, this, SLOT(purgeLog())); // explicitně zakážeme použití proxy server->setProxy(QNetworkProxy::NoProxy); // nastavíme max. počet spojení server->setMaxPendingConnections(50); // nastavíme IP a port pro naslouchání bool listening = server->listen(addr, port); // uložíme si IP adresu s portem do řetězce ve tvaru IP:port (jen pro výpis) QString ipport = addr.toString() + ":" + QString::number(port); // pokud se TCP server nepodařilo spustit na dané IP a portu if(not listening) { // vypíšeme chybovou hlášku cerr << tr("couldn't bind to %1, quitting…").arg(ipport) + "\n"; // zajistíme okamžitý zápis na stderr cerr.flush(); // tímto zajistíme ukončení programu po provedení konstruktoru QTimer::singleShot(0, qApp, SLOT(quit())); } else { cerr << tr("listening on %1").arg(ipport) + "\n"; cerr.flush(); } } // (ihned) zapíše data do socketu void TcpServer::socketWrite(QTcpSocket* socket, QByteArray data) { // zapíše data socket->write(data); // vynutí okamžitý zápis čekajících dat socket->flush(); } // vyprázdní záznam o přihlašování void TcpServer::purgeLog() { log.clear(); } // zpracuje nové příchozí spojení void TcpServer::handleNewConnection() { // získáme socket QTcpSocket* socket = server->nextPendingConnection(); if(!socket) return; cerr << tr("incoming connection from ") << socket->peerAddress().toString() << "\n"; cerr.flush(); // komunikace s klientem connect(socket, SIGNAL(readyRead()), SLOT(reply())); // šetříme paměť a zajistíme smazání socketu po jeho odpojení connect(socket, SIGNAL(disconnected()), SLOT(gotDisconnected())); // požádáme klienta, aby se přihlásil this->socketWrite(socket, "LOGIN"); } // vyřizuje odpověď na příchozí požadavky void TcpServer::reply() { // získáme socket (skrz objekt, který vyslal signál) QTcpSocket* socket = qobject_cast<QTcpSocket*>(this->sender()); if(log.value(socket->peerAddress().toString(), 0) >= 10) this->socketWrite(socket, "TOOMANYWRONGLOGINS"); // požadavek QByteArray rawdata = socket->readAll(); // argumenty požadavku QList<QByteArray> args = rawdata.split(' '); // samotný příkaz QString command = args.takeFirst(); // přihlašovací/uživatelské jméno QString user = logins.value(socket); qDebug() << rawdata; if(command == "LOGIN" || command == "FORCELOGIN") { // klient se chce přihlásit // klient je již přihlášen (na tomto socketu) if(!user.isEmpty()) { this->socketWrite(socket, "ALREADYLOGGED"); return; } // vynucené přihlášení? bool force = (command == "FORCELOGIN"); // heslo QString pass; // špatný počet argumentů nebudeme tolerovat if(args.size() == 2) { user = args.at(0); pass = args.at(1); } // zahájíme přihlašování this->login(socket, user, pass, force); return; } if(user.isEmpty()) { socket->disconnectFromHost(); QString ip(socket->peerAddress().toString()); cerr << tr("a client (IP=%1) has requested something without being logged in,").arg(ip) << "\n" << tr("which is suspicious – closing the connection.") << "\n"; cerr.flush(); } if(command == "LIST") { // klient žádá o seznam svých TODO položek this->listItems(socket); } else if(command == "ADD") { // klient si přeje přidat novou TODO položku this->addItem(socket, rawdata.right( rawdata.size()-command.size()-1 )); } else if(command == "REMOVE") { // klient si přeje odstranit položky this->removeItems(socket, rawdata.right( rawdata.size()-command.size()-1 ) .split(' ')); } else { // klient poslal neplatný příkaz this->socketWrite(socket, "INVALIDCMD"); cerr << "invalid command: " << command << "\n"; cerr.flush(); return; } } // přihlásí uživatele void TcpServer::login(QTcpSocket* socket, const QString& user, const QString& pass, bool force) { //qDebug() << "user" << user << "is trying to log in with pass" << pass; // načteme správné heslo QString realpass = settings.value(QString("logins/%1").arg(user)).toString(); // a porovnáme jej se zadaným (navíc se ujistíme, zda uživatel vůbec existuje, // abychom ošetřili přihlášení bez hesla) if(!user.isEmpty() && !realpass.isEmpty() && pass == realpass) { // ok // zjistíme, zda je uživatel již přihlášen odjinud bool alreadyLogged = logins.values().contains(user); // pokud ano a nejde o vynucený login… if(alreadyLogged and not force) { // informujeme o tom klienta this->socketWrite(socket, "ALREADYLOGGED"); return; } // pokud ano a jde o vynucený login… if(alreadyLogged) { // získáme seznam všech socketů QList<QTcpSocket*> keys = logins.keys(); // a odpojíme ten, který je přihlášený pod daným jménem for(int i=0; i<keys.size(); i++) if(logins.value(keys.at(i)) == user) { QTcpSocket* oldSocket = keys.at(i); oldSocket->disconnectFromHost(); logins.remove(oldSocket); break; } } // pošleme odpověď this->socketWrite(socket, "LOGGED"); // přiřadíme si socket k uživatelskému jménu do seznamu přihlášení logins.insert(socket, user); cerr << tr("user %1 successfully logged in").arg(user) << "\n"; cerr.flush(); } else { // špatný login this->socketWrite(socket, "WRONGLOGIN"); // odpojíme klienta socket->disconnectFromHost(); // zaznamenáme chybný login QString ip(socket->peerAddress().toString()); log.insert(ip, log.value(ip, 0)+1); cerr << tr("user %1 tried to log in with a wrong password, " "closing the connection").arg(user) << "\n"; cerr.flush(); } } // opraví klíče (indexy) položek v nastavení tak, aby šly po sobě (000 až 999) void TcpServer::sortKeys(QString user) { // získáme seznam klíčů QStringList keys = settings.allKeys(); // seznam je skutečně plný, nedá se nic dělat if(keys.size() > 999) return; cerr << tr("re-sorting %1's todo list").arg(user) + "\n"; cerr.flush(); // získáme seznam položek QStringList items; foreach(QString key, keys) items << settings.value(key).toString(); //qDebug() << "items:" << items; // odstraníme všechny položky settings.remove(""); // a přidáme je zpátky se správnými klíči for(int key = 0; key < keys.size(); key++) settings.setValue(this->getKey(user, key), items.at(key)); settings.sync(); } // vrátí klíč (index), pod kterým se položka uloží QString TcpServer::getKey(QString user, int index) { QString key = QString::number(index); if(index < 10) // pokud je index menší než 10 key.prepend("00"); // přidáme dvě nuly (5 -> 005), atd. else if(index < 100) key.prepend("0"); else if(index < 1000) Q_UNUSED(key) else { // index je vyšší než 999, zkusíme to ještě zachránit // zkusíme opravit indexy this->sortKeys(user); // načteme poslední index a přičteme k němu 1 int nIndex = settings.allKeys().last().toInt() + 1; // pokud je menší než 1000, povedlo se… if(nIndex < 1000) key = this->getKey(user, nIndex); else // prázdný klíč = plný seznam key.clear(); } return key; } // odešle klientovi seznam jeho položek void TcpServer::listItems(QTcpSocket* socket) { // načteme si uživatelské jméno QString user = logins.value(socket); // odpověď QByteArray reply; settings.beginGroup("todolist_" + user); QStringList keys = settings.allKeys(); // přidáme do odpovědi seznam všech položek foreach(QString key, keys) reply += settings.value(key).toString() + '\n'; settings.endGroup(); // ořízneme poslední \n reply.chop(1); // přidáme odpovídající příkaz if(reply.isEmpty()) reply = "LISTEMPTY"; else reply.prepend("LISTING "); //qDebug() << reply; // a odpovíme this->socketWrite(socket, reply); } // přidá položku do seznamu uživatele void TcpServer::addItem(QTcpSocket* socket, QString item) { QString user = logins.value(socket); settings.beginGroup("todolist_" + user); // zjistíme, pod kterým klíčem/indexem položku uložit int index; if(settings.allKeys().size() > 0) // k poslednímu uloženému indexu přičteme 1 index = settings.allKeys().last().toInt() + 1; else // první index bude 0 index = 0; //qDebug() << "index:" << index; // získáme klíč ve správném formátu QString key = this->getKey(user, index); // prázdný klíč = plný seznam if(key.isEmpty()) { // informujeme klienta, že má plný seznam TODO this->socketWrite(socket, "LISTFULL"); // nezapomeneme ukončit skupinu! settings.endGroup(); cerr << tr("user %1 has got a full todo list").arg(user) << "\n"; cerr.flush(); return; } // uložíme položku settings.setValue(key, item); // sestrojíme odpověď QByteArray reply = "ADDED "; reply += settings.value(key).toString(); settings.endGroup(); // odešleme odpověď this->socketWrite(socket, reply); // zapíšeme změny v TODO settings.sync(); } // odstraní vybrané položky ze seznamu void TcpServer::removeItems(QTcpSocket* socket, QList<QByteArray> keys) { QString user = logins.value(socket); QByteArray reply = "REMOVED"; settings.beginGroup("todolist_" + user); QStringList savedKeys = settings.allKeys(); foreach(QByteArray key, keys) { int index = key.toInt(); settings.remove(savedKeys.at(index)); reply += " " + key; } settings.endGroup(); settings.sync(); this->socketWrite(socket, reply); } // klient se odpojil void TcpServer::gotDisconnected() { // získáme ukazatel na objekt (socket), který vyslal signál QTcpSocket* socket = qobject_cast<QTcpSocket*>( this->sender() ); // získáme přihlašovací jméno QString user = logins.value(socket); if(user.isEmpty()) user = "*not logged*"; else // odstraníme ze seznamu aktivních připojení logins.remove(socket); // smažeme socket socket->deleteLater(); cerr << tr("a client (%1) has disconnected").arg(user) << "\n"; cerr.flush(); }
main.cpp
:
#include <QCoreApplication> #include <QDebug> #include <QHostAddress> #include <QStringList> #include <QTextCodec> #include "tcpserver.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); // výchozí port int port = 2266; // výchozí adresa pro naslouchání QHostAddress addr(QHostAddress::Any); // je vše v pořádku pro spuštění programu? bool ok = true; // kontrola argumentů na příkazovém řádku if(argc > 3) { ok = false; } else { if(argc == 3) port = a.arguments().at(2).toInt(&ok); if(argc > 1) ok = addr.setAddress(a.arguments().at(1)); } if(not ok) { qDebug() << "usage:" << argv[0] << "[IP(0.0.0.0)] [port(2266)]"; return 1; } a.setApplicationName("todoserver"); a.setOrganizationDomain("watzke.cz"); // nastavíme kódování C řetězců QTextCodec::setCodecForCStrings( QTextCodec::codecForName("UTF-8") ); TcpServer s(addr, port); return a.exec(); }
Zdrojáky si můžete stáhnout v archívu tcpserver.tar.bz2.
ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.