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