Portál AbcLinuxu, 7. května 2024 20:58

Konzolové programy v Qt 4 – 3 (TCP server)

16. 9. 2009 | David Watzke
Články - Konzolové programy v Qt 4 – 3 (TCP server)  

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.

TCP server

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);

TODO server

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.

Seriál Qt 4 – Konzolové programy (dílů: 4)

První díl: Konzolové programy v Qt 4 – 1 (úvod), poslední díl: Konzolové programy v Qt 4 – 4 (UDP server a klient).
Předchozí díl: Konzolové programy v Qt 4 – 2 (práce s HTTP a FTP)
Následující díl: Konzolové programy v Qt 4 – 4 (UDP server a klient)

Související články

Seriál: Qt 4 - psaní grafických programů
Cmake: zjednoduš si život
Seriál: Kommander
Seriál: KDE: tipy a triky
Seriál: Začíname KProgramovať

Odkazy a zdroje

qt.nokia.com

Další články z této rubriky

LLVM a Clang – více než dobrá náhrada za GCC
Ze 4 s na 0,9 s – programovací jazyk Vala v praxi
Reverzujeme ovladače pro USB HID zařízení
Linux: systémové volání splice()
Programování v jazyce Vala - základní prvky jazyka

ISSN 1214-1267, (c) 1999-2007 Stickfish s.r.o.