PageRenderTime 223ms CodeModel.GetById 41ms app.highlight 91ms RepoModel.GetById 50ms app.codeStats 1ms

/thirdparty/qxt/qxtweb-standalone/qxtweb/qxtwebcgiservice.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 427 lines | 256 code | 30 blank | 141 comment | 48 complexity | 739d44033f30e0c61ff1a406b5543ba6 MD5 | raw file
  1/****************************************************************************
  2 **
  3 ** Copyright (C) Qxt Foundation. Some rights reserved.
  4 **
  5 ** This file is part of the QxtWeb module of the Qxt library.
  6 **
  7 ** This library is free software; you can redistribute it and/or modify it
  8 ** under the terms of the Common Public License, version 1.0, as published
  9 ** by IBM, and/or under the terms of the GNU Lesser General Public License,
 10 ** version 2.1, as published by the Free Software Foundation.
 11 **
 12 ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
 13 ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
 14 ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
 15 ** FITNESS FOR A PARTICULAR PURPOSE.
 16 **
 17 ** You should have received a copy of the CPL and the LGPL along with this
 18 ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
 19 ** included with the source distribution for more information.
 20 ** If you did not receive a copy of the licenses, contact the Qxt Foundation.
 21 **
 22 ** <http://libqxt.org>  <foundation@libqxt.org>
 23 **
 24 ****************************************************************************/
 25
 26/*!
 27\class QxtWebCgiService
 28
 29\inmodule QxtWeb
 30
 31\brief The QxtWebCgiService class provides a CGI/1.1 gateway for QxtWeb
 32
 33TODO: write docs
 34TODO: implement timeout
 35*/
 36
 37#include "qxtwebcgiservice.h"
 38#include "qxtwebcgiservice_p.h"
 39#include "qxtwebevent.h"
 40#include "qxtwebcontent.h"
 41#include <QMap>
 42#include <QFile>
 43#include <QProcess>
 44#include <QtDebug>
 45
 46QxtCgiRequestInfo::QxtCgiRequestInfo() : sessionID(0), requestID(0), eventSent(false), terminateSent(false) {}
 47QxtCgiRequestInfo::QxtCgiRequestInfo(QxtWebRequestEvent* req) : sessionID(req->sessionID), requestID(req->requestID), eventSent(false), terminateSent(false) {}
 48
 49/*!
 50 * Constructs a QxtWebCgiService object with the specified session \a manager and \a parent.
 51 * This service will invoke the specified \a binary to handle incoming requests.
 52 *
 53 * Often, the session manager will also be the parent, but this is not a requirement.
 54 */
 55QxtWebCgiService::QxtWebCgiService(const QString& binary, QxtAbstractWebSessionManager* manager, QObject* parent) : QxtAbstractWebService(manager, parent)
 56{
 57    QXT_INIT_PRIVATE(QxtWebCgiService);
 58    qxt_d().binary = binary;
 59    QObject::connect(&qxt_d().timeoutMapper, SIGNAL(mapped(QObject*)), &qxt_d(), SLOT(terminateProcess(QObject*)));
 60}
 61
 62/*!
 63 * Returns the path to the CGI script that will be executed to handle requests.
 64 *
 65 * \sa setBinary()
 66 */
 67QString QxtWebCgiService::binary() const
 68{
 69    return qxt_d().binary;
 70}
 71
 72/*!
 73 * Sets the path to the CGI script \a bin that will be executed to handle requests.
 74 *
 75 * \sa binary()
 76 */
 77void QxtWebCgiService::setBinary(const QString& bin)
 78{
 79    if (!QFile::exists(bin) || !(QFile::permissions(bin) & (QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther)))
 80    {
 81        qWarning() << "QxtWebCgiService::setBinary: " + bin + " does not appear to be executable.";
 82    }
 83    qxt_d().binary = bin;
 84}
 85
 86/*!
 87 * Returns the maximum time a CGI script may execute, in milliseconds.
 88 *
 89 * The default value is 0, which indicates that CGI scripts will not be terminated
 90 * due to long running times.
 91 *
 92 * \sa setTimeout()
 93 */
 94int QxtWebCgiService::timeout() const
 95{
 96    return qxt_d().timeout;
 97}
 98
 99/*!
100 * Sets the maximum \a time a CGI script may execute, in milliseconds.
101 *
102 * The timer is started when the script is launched. After the timeout elapses once,
103 * the script will be asked to stop, as QProcess::terminate(). (That is, the script
104 * will receive WM_CLOSE on Windows or SIGTERM on UNIX.) If the process has still
105 * failed to terminate after another timeout, it will be forcibly terminated, as
106 * QProcess::kill(). (That is, the script will receive TerminateProcess on Windows
107 * or SIGKILL on UNIX.)
108 *
109 * Set the timeout to 0 to disable this behavior; scripts will not be terminated
110 * due to excessive run time. This is the default behavior.
111 *
112 * CAUTION: Keep in mind that the timeout applies to the real running time of the
113 * script, not processor time used. A script that initiates a lengthy download 
114 * may be interrupted while transferring data to the web browser. To avoid this
115 * behavior, see the timeoutOverride property to allow the script to request
116 * an extended timeout, or use a different QxtAbstractWebService object for
117 * serving streaming content or large files.
118 *
119 *
120 * \sa timeout(), timeoutOverride(), setTimeoutOverride(), QProcess::terminate(), QProcess::kill()
121 */
122void QxtWebCgiService::setTimeout(int time)
123{
124    qxt_d().timeout = time;
125}
126
127/*!
128 * Returns whether or not to allow scripts to override the timeout.
129 *
130 * \sa setTimeoutOverride(), setTimeout()
131 */
132bool QxtWebCgiService::timeoutOverride() const
133{
134    return qxt_d().timeoutOverride;
135}
136
137/*!
138 * Sets whether or not to allow scripts to override the timeout.
139 * Scripts are allowed to override if \a enable is \c true.
140 *
141 * As an extension to the CGI/1.1 gateway specification, a CGI script may
142 * output a "X-QxtWeb-Timeout" header to change the termination timeout
143 * on a per-script basis. Only enable this option if you trust the scripts
144 * being executed.
145 *
146 * \sa timeoutOverride(), setTimeout()
147 */
148void QxtWebCgiService::setTimeoutOverride(bool enable)
149{
150    qxt_d().timeoutOverride = enable;
151}
152
153/*!
154 * \reimp
155 */
156void QxtWebCgiService::pageRequestedEvent(QxtWebRequestEvent* event)
157{
158    // Create the process object and initialize connections
159    QProcess* process = new QProcess(this);
160    qxt_d().requests[process] = QxtCgiRequestInfo(event);
161    qxt_d().processes[event->content] = process;
162    QxtCgiRequestInfo& requestInfo = qxt_d().requests[process];
163    QObject::connect(process, SIGNAL(readyRead()), &qxt_d(), SLOT(processReadyRead()));
164    QObject::connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), &qxt_d(), SLOT(processFinished()));
165    QObject::connect(process, SIGNAL(error(QProcess::ProcessError)), &qxt_d(), SLOT(processFinished()));
166    requestInfo.timeout = new QTimer(process);
167    qxt_d().timeoutMapper.setMapping(requestInfo.timeout, process);
168    QObject::connect(requestInfo.timeout, SIGNAL(timeout()), &qxt_d().timeoutMapper, SLOT(map()));
169
170    // Initialize the system environment
171    QStringList s_env = process->systemEnvironment();
172    QMap<QString, QString> env;
173    foreach(const QString& entry, s_env)
174    {
175        int pos = entry.indexOf('=');
176        env[entry.left(pos)] = entry.mid(pos + 1);
177    }
178
179    // Populate CGI/1.1 environment variables
180    env["SERVER_SOFTWARE"] = QString("QxtWeb/" QXT_VERSION_STR);
181    env["SERVER_NAME"] = event->url.host();
182    env["GATEWAY_INTERFACE"] = "CGI/1.1";
183    if (event->headers.contains("X-Request-Protocol"))
184        env["SERVER_PROTOCOL"] = event->headers.value("X-Request-Protocol");
185    else
186        env.remove("SERVER_PROTOCOL");
187    if (event->url.port() != -1)
188        env["SERVER_PORT"] = QString::number(event->url.port());
189    else
190        env.remove("SERVER_PORT");
191    env["REQUEST_METHOD"] = event->method;
192    env["PATH_INFO"] = event->url.path();
193    env["PATH_TRANSLATED"] = event->url.path(); // CGI/1.1 says we should resolve this, but we have no logical interpretation
194    env["SCRIPT_NAME"] = event->originalUrl.path().remove(QRegExp(QRegExp::escape(event->url.path()) + '$'));
195    env["SCRIPT_FILENAME"] = qxt_d().binary;    // CGI/1.1 doesn't define this but PHP demands it
196    env.remove("REMOTE_HOST");
197    env["REMOTE_ADDR"] = event->remoteAddress;
198    // TODO: If we ever support HTTP authentication, we should use these
199    env.remove("AUTH_TYPE");
200    env.remove("REMOTE_USER");
201    env.remove("REMOTE_IDENT");
202    if (event->contentType.isEmpty())
203    {
204        env.remove("CONTENT_TYPE");
205        env.remove("CONTENT_LENGTH");
206    }
207    else
208    {
209        env["CONTENT_TYPE"] = event->contentType;
210        env["CONTENT_LENGTH"] = QString::number(event->content->unreadBytes());
211    }
212    env["QUERY_STRING"] = event->url.encodedQuery();
213
214    // Populate HTTP header environment variables
215    QMultiHash<QString, QString>::const_iterator iter = event->headers.constBegin();
216    while (iter != event->headers.constEnd())
217    {
218        QString key = "HTTP_" + iter.key().toUpper().replace('-', '_');
219        if (key != "HTTP_CONTENT_TYPE" && key != "HTTP_CONTENT_LENGTH")
220            env[key] = iter.value();
221        iter++;
222    }
223
224    // Populate HTTP_COOKIE parameter
225    iter = event->cookies.constBegin();
226    QString cookies;
227    while (iter != event->cookies.constEnd())
228    {
229        if (!cookies.isEmpty())
230            cookies += "; ";
231        cookies += iter.key() + '=' + iter.value();
232        iter++;
233    }
234    if (!cookies.isEmpty())
235        env["HTTP_COOKIE"] = cookies;
236
237    // Load environment into process space
238    QStringList p_env;
239    QMap<QString, QString>::iterator env_iter = env.begin();
240    while (env_iter != env.end())
241    {
242        p_env << env_iter.key() + '=' + env_iter.value();
243        env_iter++;
244    }
245    process->setEnvironment(p_env);
246
247    // Launch process
248    if (event->url.hasQuery() && event->url.encodedQuery().contains('='))
249    {
250        // CGI/1.1 spec says to pass the query on the command line if there's no embedded = sign
251        process->start(qxt_d().binary + ' ' + QUrl::fromPercentEncoding(event->url.encodedQuery()), QIODevice::ReadWrite);
252    }
253    else
254    {
255        process->start(qxt_d().binary, QIODevice::ReadWrite);
256    }
257
258    // Start the timeout
259    if(qxt_d().timeout > 0)
260    {
261        requestInfo.timeout->start(qxt_d().timeout);
262    }
263
264    // Transmit POST data
265    if (event->content)
266    {
267        QObject::connect(event->content, SIGNAL(readyRead()), &qxt_d(), SLOT(browserReadyRead()));
268        qxt_d().browserReadyRead(event->content);
269    }
270}
271
272/*!
273 * \internal
274 */
275void QxtWebCgiServicePrivate::browserReadyRead(QObject* o_content)
276{
277    if (!o_content) o_content = sender();
278    QxtWebContent* content = static_cast<QxtWebContent*>(o_content); // this is a private class, no worries about type safety
279
280    // Read POST data and copy it to the process
281    QByteArray data = content->readAll();
282    if (!data.isEmpty())
283        processes[content]->write(data);
284
285    // If no POST data remains unsent, clean up
286    if (!content->unreadBytes() && processes.contains(content))
287    {
288        processes[content]->closeWriteChannel();
289        processes.remove(content);
290    }
291}
292
293/*!
294 * \internal
295 */
296void QxtWebCgiServicePrivate::processReadyRead()
297{
298    QProcess* process = static_cast<QProcess*>(sender());
299    QxtCgiRequestInfo& request = requests[process];
300
301    QByteArray line;
302    while (process->canReadLine())
303    {
304        // Read in a CGI/1.1 header line
305        line = process->readLine().replace(QByteArray("\r"), ""); //krazy:exclude=doublequote_chars
306        if (line == "\n")
307        {
308            // An otherwise-empty line indicates the end of CGI/1.1 headers and the start of content
309            QObject::disconnect(process, SIGNAL(readyRead()), this, 0);
310            QxtWebPageEvent* event = 0;
311            int code = 200;
312            if (request.headers.contains("status"))
313            {
314                // CGI/1.1 defines a "Status:" header that dictates the HTTP response code
315                code = request.headers["status"].left(3).toInt();
316                if (code >= 300 && code < 400)  // redirect
317                {
318                    event = new QxtWebRedirectEvent(request.sessionID, request.requestID, request.headers["location"], code);
319                }
320            }
321            // If a previous header (currently just status) hasn't created an event, create a normal page event here
322            if (!event)
323            {
324                event = new QxtWebPageEvent(request.sessionID, request.requestID, QSharedPointer<QIODevice>(process) );
325                event->status = code;
326            }
327            // Add other response headers passed from CGI (currently only Content-Type is supported)
328            if (request.headers.contains("content-type"))
329                event->contentType = request.headers["content-type"].toUtf8();
330            // TODO: QxtWeb doesn't support transmitting arbitrary HTTP headers right now, but it may be desirable
331            // for applications that know what kind of server frontend they're using to allow scripts to send
332            // protocol-specific headers.
333            
334            // Post the event
335            qxt_p().postEvent(event);
336            request.eventSent = true;
337            return;
338        }
339        else
340        {
341            // Since we haven't reached the end of headers yet, parse a header
342            int pos = line.indexOf(": ");
343            QByteArray hdrName = line.left(pos).toLower();
344            QByteArray hdrValue = line.mid(pos + 2).replace(QByteArray("\n"), ""); //krazy:exclude=doublequote_chars
345            if (hdrName == "set-cookie")
346            {
347                // Parse a new cookie and post an event to send it to the client
348                QList<QByteArray> cookies = hdrValue.split(',');
349                foreach(const QByteArray& cookie, cookies)
350                {
351                    int equals = cookie.indexOf("=");
352                    int semi = cookie.indexOf(";");
353                    QByteArray cookieName = cookie.left(equals);
354                    int age = cookie.toLower().indexOf("max-age=", semi);
355                    int secs = -1;
356                    if (age >= 0)
357                        secs = cookie.mid(age + 8, cookie.indexOf(";", age) - age - 8).toInt();
358                    if (secs == 0)
359                    {
360                        qxt_p().postEvent(new QxtWebRemoveCookieEvent(request.sessionID, cookieName));
361                    }
362                    else
363                    {
364                        QByteArray cookieValue = cookie.mid(equals + 1, semi - equals - 1);
365                        QDateTime cookieExpires;
366                        if (secs != -1)
367                            cookieExpires = QDateTime::currentDateTime().addSecs(secs);
368                        qxt_p().postEvent(new QxtWebStoreCookieEvent(request.sessionID, cookieName, cookieValue, cookieExpires));
369                    }
370                }
371            }
372            else if(hdrName == "x-qxtweb-timeout")
373            {
374                if(timeoutOverride)
375                    request.timeout->setInterval(hdrValue.toInt());
376            }
377            else
378            {
379                // Store other headers for later inspection
380                request.headers[hdrName] = hdrValue;
381            }
382        }
383    }
384}
385
386/*!
387 * \internal
388 */
389void QxtWebCgiServicePrivate::processFinished()
390{
391    QProcess* process = static_cast<QProcess*>(sender());
392    QxtCgiRequestInfo& request = requests[process];
393
394    if (!request.eventSent)
395    {
396        // If no event was posted, issue an internal error
397        qxt_p().postEvent(new QxtWebErrorEvent(request.sessionID, request.requestID, 500, "Internal Server Error"));
398    }
399
400    // Clean up data structures
401    process->close();
402    QxtWebContent* key = processes.key(process);
403    if (key) processes.remove(key);
404    timeoutMapper.removeMappings(request.timeout);
405    requests.remove(process);
406}
407
408/*!
409 * \internal
410 */
411void QxtWebCgiServicePrivate::terminateProcess(QObject* o_process)
412{
413    QProcess* process = static_cast<QProcess*>(o_process);
414    QxtCgiRequestInfo& request = requests[process];
415
416    if(request.terminateSent)
417    {
418        // kill with fire
419        process->kill();
420    }
421    else
422    {
423        // kill nicely
424        process->terminate();
425        request.terminateSent = true;
426    }
427}