#include "webclient.h" #include "webclientserver.h" #include const QByteArray &eTagVersion = "v1-"; HttpRequest::HttpRequest() { } HttpRequest::HttpRequest(const QList &text) :m_text(text) { parseText(); } HttpRequest::HttpRequest(QTcpSocket *socket) :m_socket(socket) { readText(); parseText(); } QByteArray HttpRequest::path() { return m_path; } QByteArray HttpRequest::cookies() { return m_cookies; } QList HttpRequest::parsedCookies() { return m_parsedCookies; } QByteArray HttpRequest::hostName() { return m_hostName; } void HttpRequest::readText() { // TODO fix denial-of-service attack while (m_socket->canReadLine()) { m_text.append(m_socket->readLine()); } // DEBUG << "req" << m_text; } void HttpRequest::parseText() { foreach (const QByteArray &line, m_text) { if (line.startsWith("GET")) { m_path = QUrl::fromPercentEncoding(line.mid(4).split(' ').at(0)).toAscii(); // ### assumes well-formed string } else if (line.startsWith("POST")) { m_path = QUrl::fromPercentEncoding(line.mid(5).split(' ').at(0)).toAscii(); // ### assumes well-formed string } else if (line.startsWith("Cookie:")) { // qDebug() << "cookie line" << line.simplified(); m_cookies = line.mid(7).simplified(); // remove "Cookie:" // qDebug() << "cookies text" << m_cookies; foreach (const QByteArray cookieText, m_cookies.split(';')){ if (cookieText.contains('=')) { QList cookieParts = cookieText.split('='); QNetworkCookie cookie(cookieParts.at(0).simplified(), cookieParts.at(1).simplified()); m_parsedCookies.append(cookie); } } } else if (line.startsWith("Host")) { QByteArray hostline = line.split(' ').at(1); // ### hostline.chop(2); // remove newline m_hostName = hostline; } else if (line.startsWith("If-None-Match:")){ m_ifNoneMatch = line.mid(18).simplified(); // remove "If-None-Match: vX-", where X is the integer version number } } } HttpResponse::HttpResponse() : contentType("text/html"), response304(false) { } void HttpResponse::setBody(const QByteArray &body) { this->body = body; } void HttpResponse::setCookie(const QByteArray &name, const QByteArray &value) { cookie = (name +"="+ value); } void HttpResponse::setContentType(const QByteArray &contentType) { this->contentType = contentType; } void HttpResponse::seteTag(const QByteArray &eTag) { this->eTag = eTagVersion + eTag; } void HttpResponse::set304Response() { response304 = true; } bool HttpResponse::isHandled() const { return response304 || body.isEmpty() == false; } QByteArray HttpResponse::toText() { time_t currentTime = time(0); QByteArray text; if (response304) { text += QByteArray("HTTP/1.1 304 Not Modified\r\n"); text+= QByteArray("\r\n"); return text; } text += QByteArray("HTTP/1.1 200 OK \r\n"); text += QByteArray("Date: ") + QByteArray(asctime(gmtime(¤tTime))) + QByteArray("") + QByteArray("Content-Type: " + contentType + " \r\n") + QByteArray("Content-Length: " + QByteArray::number(body.length()) + "\r\n"); if (cookie.isEmpty() == false) { text+= "Set-Cookie: " + cookie + "\r\n"; } if (eTag.isEmpty() == false) { text += "eTag: " + eTag + "\r\n"; } else { text += QByteArray("Cace-control: no-cache \r\n"); text += QByteArray("max-age: 0 \r\n"); } text+= QByteArray("\r\n") + body; return text; } Session::Session(Server *server, int sessionId) :m_sessionId(sessionId), m_idleSocket(0), m_server(server) { } int Session::sessionId() { return m_sessionId; } void Session::setIdleSocket(QTcpSocket *socket) { m_idleSocket = socket; } QTcpSocket * Session::idleSocket() { return m_idleSocket; } void Session::emitRequestContent(HttpRequest *request, HttpResponse *response) { emit requestContent(request, response); } void Session::contentAvailable() { m_server->contentAvailable(this); } void Session::idleSocketDisconnect() { // DEBUG << "idleSocketDisconnect"; m_idleSocket = 0; } Server::Server(quint16 port) { this->port = port; connect(this, SIGNAL(newConnection()), SLOT(connectionAvailable())); listen(QHostAddress::Any, port); qDebug() << QString("Server running on: http://" + QHostInfo::localHostName() + ":" + QString::number(port) + "/"); qsrand(QDateTime::currentDateTime().toTime_t()); nextCookieId = qrand(); dynamicBytesWritten = 0; staticBytesWritten = 0; bytesRead = 0; serverStart = QDateTime::currentDateTime(); totalSessions = 0; activeSessionLimitHtml = "Active session limit exceeded."; } Server::~Server() { qDebug() << QString("Server stopped."); } void Server::printRequest(const QList &request) { foreach (const QByteArray &line, request) { DEBUG << line; } } void Server::contentAvailable(Session *session) { // DEBUG << "content available!"; QTcpSocket *socket = session->m_idleSocket; if (socket == 0) return; disconnect(socket, SIGNAL(disconnected()), session, SLOT(idleSocketDisconnect())); session->m_idleSocket = 0; session->emitRequestContent(&session->m_idleRequest, &session->m_idleResponse); socket->write(session->m_idleResponse.toText()); } void Server::connectionAvailable() { QTcpSocket *socket = nextPendingConnection(); connect(socket, SIGNAL(readyRead()), this, SLOT(dataOnSocket())); // ### race condition? } void Server::dataOnSocket() { QTcpSocket * socket = static_cast(sender()); DEBUG << ""; DEBUG << "request"; QList lines; while (socket->canReadLine()) { QByteArray line = socket->readLine(); lines.append(line); bytesRead += line.count(); } DEBUG << lines; HttpRequest request(lines); int sessionId = 0; DEBUG << "cookies" << request.cookies(); foreach (QNetworkCookie cookie, request.parsedCookies()) { if (cookie.name() == "qtcookie") { sessionId = cookie.value().toInt(); } } if (sessionId == 0 && request.path().contains("favicon.ico")) { // Helloo Opera, which request favicon.ico without setting // the session id cookie. HttpResponse response; socket->write(response.toText()); return; } DEBUG << "sessionId" << sessionId; HttpResponse response; Session *session = activeSessions.value(sessionId); if (session == 0) { // ### accept unknown sessions for now, TODO do authentication here. DEBUG << "new session for" << sessionId; if (totalSessions >= activeSessionLimit) { dynamicBytesWritten += activeSessionLimitHtml.size(); socket->write(activeSessionLimitHtml); socket->disconnectFromHost(); return; } ++totalSessions; sessionId = nextCookieId; nextCookieId = qrand(); // ### response.setCookie("qtcookie", QByteArray::number(sessionId)); // set new. session = new Session(this, sessionId); session->address = socket->peerAddress(); activeSessions.insert(sessionId, session); // DEBUG << "new session" << sessionId << session; emit sessionBegin(session); } else { // DEBUG << "found session for" << sessionId; } // Strip away the page ids: "-pageId=" /* int index = request.m_path.indexOf("-pageId="); if (index != -1) { request.m_path.chop(request.m_path.count() - index); } */ session->emitRequestContent(&request, &response); if (response.isHandled()) { QByteArray responseText = response.toText(); dynamicBytesWritten += responseText.count(); socket->write(responseText); return; } const QByteArray path = request.path(); // The "/idle" request signals that the content server can send more events. // If there are no more events, save this connection and keep it open. // if (path == "/idle") // DEBUG << response.body; if (path.startsWith("/idle") && response.isHandled() == false) { // Keep one socket for each connection, the html spec allows // only two connections between a web browser and a server. if (session->m_idleSocket == 0) { connect(socket, SIGNAL(disconnected()), session, SLOT(idleSocketDisconnect())); session->m_idleSocket = socket; session->m_idleRequest = request; session->m_idleResponse = response; } // DEBUG << "idle socket" << socket; return; } else if (path.startsWith("/statistics")) { response.setBody(createStatiticsPage()); } else if (response.isHandled() == false) { fileServer.handleRequest(&request, &response); QByteArray responseText = response.toText(); staticBytesWritten += responseText.count(); socket->write(responseText); return; } QByteArray responseText = response.toText(); socket->write(responseText); // DEBUG << "socket write response"; // DEBUG << "response" << response.toText(); // DEBUG << "socket write response done"; } QByteArray Server::createStatiticsPage() { const double ec2DataRate=0.02 / (1000 * 1000 * 1000) ; // cost per byte const double ec2InstanceRate = 0.1; // cost per hour const double upHours = (QDateTime::currentDateTime().toTime_t() - serverStart.toTime_t() / (60.0 * 60.0)); qDebug() << "up" << upHours << QDateTime::currentDateTime().toTime_t(); QByteArray stats; stats += " Statistics

"; stats += ""; stats += ""; stats += ""; stats += ""; stats += ""; stats += ""; stats += ""; stats += "
  Size AWS Cost
Uptime "+ QByteArray::number(upHours) + " Hours €" + QByteArray::number(ceil(upHours) * ec2InstanceRate) + "
Received "+ QByteArray::number(bytesRead / 1024) + " K €" + QByteArray::number(bytesRead * ec2DataRate) + "
Sent (dynamic content) "+ QByteArray::number(dynamicBytesWritten / 1024) + " K €" + QByteArray::number(dynamicBytesWritten * ec2DataRate) + "
Sent (static content) "+ QByteArray::number(staticBytesWritten / 1024) + " K €" + QByteArray::number(staticBytesWritten * ec2DataRate) + "
Grand Total "+ QByteArray::number((bytesRead + dynamicBytesWritten + staticBytesWritten) / 1024) + " K €" + QByteArray::number(upHours * ec2InstanceRate + (bytesRead + dynamicBytesWritten + staticBytesWritten) * ec2DataRate) + "
"; stats += "
Sessions:
"; stats += "Active :" + QByteArray::number(activeSessions.count()) + "
"; stats += "Total :" + QByteArray::number(totalSessions) + "
"; return stats; } FileServer::FileServer() { allowedFileNames = QSet() << ":index.html" << ":qwebclient.js" << ":qwebclient.css" << ":dojo.js" << ":json2.js" << ":draghandler.js" << ":sessionhandler.js" << ":eventhandler.js"; } void FileServer::handleRequest(HttpRequest *request, HttpResponse *response) { if (response->isHandled()) return; const QByteArray path = request->path(); QByteArray filePath = path.right(path.size() - 1); // remove leading '/' DEBUG << "file server handle request" << path << filePath; if (filePath == "" || filePath == "index.html") filePath = ":index.html"; if (allowedFileNames.contains(filePath) == false) return; // ### drop connection? QFile file(filePath); if (file.exists() == false) { // DEBUG << "no file" << filePath; return; } // Check if the client sends an If-None-Match, return // 304 Not Modified if it matches the server's eTag // for the file path. if (request->m_ifNoneMatch.isEmpty() == false) { if (request->m_ifNoneMatch == eTags.value(filePath)) { response->set304Response(); response->seteTag(request->m_ifNoneMatch); return; } } file.open(QIODevice::ReadOnly); QByteArray fileContents = file.readAll(); fileContents.replace("INSERT_HOSTNAME", request->hostName()); static int pageId = 0; /* if (fileContents.contains("INSERT_PAGE_ID")) fileContents.replace("INSERT_PAGE_ID", QByteArray::number(++pageId)); */ QByteArray eTag = eTags[filePath]; if (eTag.isEmpty()) { eTag = QByteArray::number(qHash(fileContents)); eTags[filePath] = eTag; } response->seteTag(eTag); response->setBody(fileContents); }