/**************************************************************************** ** ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). ** Contact: Qt Software Information (qt-info@nokia.com) ** ** This file is part of the WebClient project on Trolltech Labs. ** ** This file may be used under the terms of the GNU General Public ** License version 2.0 or 3.0 as published by the Free Software Foundation ** and appearing in the file LICENSE.GPL included in the packaging of ** this file. �Please review the following information to ensure GNU ** General Public Licensing requirements will be met: ** http://www.fsf.org/licensing/licenses/info/GPLv2.html and ** http://www.gnu.org/copyleft/gpl.html. ** ** If you are unsure which license is appropriate for your use, please ** contact the sales department at qt-sales@nokia.com. ** ** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE ** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. ** ****************************************************************************/ #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:")) { // DEBUG << "cookie line" << line.simplified(); m_cookies = line.mid(7).simplified(); // remove "Cookie:" // DEBUG << "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), neverExpires(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) { if (eTag.isEmpty()) { this->eTag.clear(); return; } this->eTag = eTagVersion + eTag; } void HttpResponse::set304Response() { response304 = true; } void HttpResponse::setNeverExpires() { neverExpires = 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"; } // Support three different caching strategies: // 1. Never-expires. Useful when content has a unique // name based on e.g. a hash of the content. Allows the // user agent to get the content once and then cache it // forever. // 2. Etag. The url for the content stays the same, the // etag changes with content changes. Allows the user agent // to ask if a spesific url as been updated, and then skip // the content download if not. // 3. no-cache. For dynamic content. Not cached by the user-agent // and is re-downloaded on each request. if (neverExpires) { text += QByteArray("Cache-control: max-age=9999999 \r\n"); // or -1? text += "eTag: 24 \r\n"; } else if (eTag.isEmpty() == false) { text += "eTag: " + eTag + "\r\n"; } else { text += QByteArray("Cace-control: no-cache \r\n"); text += QByteArray("Cache-control: 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) { lastActivityTime = QDateTime::currentDateTime(); } 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); DEBUG << 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; activeSessionLimit = INT_MAX; activeSessionLimitHtml = "Active session limit exceeded."; inactiveSessionTimeout = 60 * 10; connect(&purgeInactiveSessionsTimer, SIGNAL(timeout()), SLOT(purgeInactiveSessions())); purgeInactiveSessionsTimer.start(1000 / 1); // This didn't work out. Disable for now. sendUpdatesForPlainQWidgets = true; // skipUpdatesClasses.insert("QWidget"); // skipUpdatesClasses.insert("QRcui"); } 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!"; // Check if there is a long-polling socket available. // If not, then no content can be sent at this point. if (session->m_idleSocket == 0) return; // The socket is no longer the idle socket. QTcpSocket *socket = session->m_idleSocket; disconnect(socket, SIGNAL(disconnected()), session, SLOT(idleSocketDisconnect())); session->m_idleSocket = 0; // Get content from the session and write it to the socket. 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 << "create new session"; 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; } session->lastActivityTime = QDateTime::currentDateTime(); // 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"; } void Server::purgeInactiveSessions() { DEBUG << "purgeInactiveSessions"; QDateTime now = QDateTime::currentDateTime(); // find where we left off the last time, restart from // the beginning if not found. QHash::iterator it = activeSessions.find(lastSessionVisited); if (it == activeSessions.end()) it = activeSessions.begin(); const int maxSessonsToExamine = 50; // avoid pausing to long. int i = 0; while (it != activeSessions.end()) { if (i > maxSessonsToExamine) { lastSessionVisited = it.key(); return; } Session *session = it.value(); DEBUG << "last act" << session << session->lastActivityTime; int inactiveSeconds = now.toTime_t() - session->lastActivityTime.toTime_t(); DEBUG << "inactive" << inactiveSeconds; if (inactiveSeconds > inactiveSessionTimeout) { QHash::iterator itToDelete = it; // get the next key so the search can continue after the // erase. (all iterators are invalidated.) ++it; if (it == activeSessions.end()) { delete itToDelete.value(); activeSessions.erase(itToDelete); it = activeSessions.end(); } else { int newKey = it.key(); delete itToDelete.value(); activeSessions.erase(itToDelete); it = activeSessions.find(newKey); } continue; // it has been repositioned. (i has not been incremented - // there is no limit to how many session we can delete in one go.) } ++it; ++i; } // reached the end, restart for next time. lastSessionVisited = -1; //actually a valid session value, but most likely a miss. } 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)); DEBUG << "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; } bool Server::shouldSkipUpdate(const QByteArray &className) { return skipUpdatesClasses.contains(className); } bool Server::testHint(QWidget *widget, int widgetHint) { return (widgetHints.contains(widget) && widgetHints.value(widget).contains(widgetHint)); } FileServer::FileServer() { allowedFileNames = QSet() << ":index.html" << ":qwebclient.js" << ":qwebclient.css" << ":dojo.js" << ":json2.js" << ":sessionhandler.js" << ":eventhandler.js" << ":firebug-lite-compressed.js" << ":firebug-lite.css"; foreach (const QString &fileName, allowedFileNames) { QFile file(fileName); if (file.exists() == false) { // DEBUG << "no file" << filePath; continue; } file.open(QIODevice::ReadOnly); QByteArray fileContents = file.readAll(); //fileContents.replace("INSERT_HOSTNAME", request->hostName()); fileData[fileName] = fileContents; fileDataDeflated[fileName] = qCompress(fileContents); eTags[fileName] = QByteArray::number(qHash(fileContents)); } } 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? // 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; } } response->seteTag(eTags[filePath]); response->setBody(fileData[filePath]); }