MythTV  master
serverSideScripting.cpp
Go to the documentation of this file.
1 // Program Name: serverSideScripting.cpp
3 // Created : Mar. 22, 2011
4 //
5 // Purpose : Server Side Scripting support for Html Server
6 //
7 // Copyright (c) 2011 David Blain <dblain@mythtv.org>
8 //
9 // Licensed under the GPL v2 or later, see COPYING for details
10 //
12 
13 #include <QCoreApplication>
14 #include <QFile>
15 #include <QFileInfo>
16 #include <QVariant>
17 #include <QVariantMap>
18 
19 #include "serverSideScripting.h"
20 #include "mythlogging.h"
21 #include <mythsession.h>
22 #include "httpserver.h"
23 
24 QScriptValue formatStr(QScriptContext *context, QScriptEngine *interpreter);
25 
27 //
29 
30 QScriptValue formatStr(QScriptContext *context, QScriptEngine *interpreter)
31 {
32  unsigned int count = context->argumentCount();
33 
34  if (count == 0)
35  return QScriptValue(interpreter, QString());
36 
37  if (count == 1)
38  return QScriptValue(interpreter, context->argument(0).toString());
39 
40  QString result = context->argument(0).toString();
41  for (unsigned int i = 1; i < count; i++)
42  result.replace(QString("%%1").arg(i), context->argument(i).toString());
43 
44  return QScriptValue(interpreter, result);
45 }
46 
48 //
50 
52 {
53  Lock();
54 
55 #ifdef _WIN32
56  m_debugger.attachTo( &m_engine );
57 #endif
58 
59  // ----------------------------------------------------------------------
60  // Enable Translation functions
61  // ----------------------------------------------------------------------
62 
63  m_engine.installTranslatorFunctions();
64 
65  // ----------------------------------------------------------------------
66  // Register C++ functions
67  // ----------------------------------------------------------------------
68 
69  QScriptValue qsFormatStr = m_engine.newFunction(formatStr);
70  m_engine.globalObject().setProperty("formatStr", qsFormatStr);
71 
72  // ----------------------------------------------------------------------
73  // Add Scriptable Objects
74  // ----------------------------------------------------------------------
75 
76  // Q_SCRIPT_DECLARE_QMETAOBJECT( DTC::MythService, QObject*)
77  // QScriptValue oClass = engine.scriptValueFromQMetaObject< DTC::MythService >();
78  // engine.globalObject().setProperty("Myth", oClass);
79  Unlock();
80 }
81 
83 //
85 
87 {
88  Lock();
89 
90  QMap<QString, ScriptInfo*>::iterator it = m_mapScripts.begin();
91 
92  for (; it != m_mapScripts.end(); ++it)
93  {
94  if (*it)
95  delete (*it);
96  }
97 
98  m_mapScripts.clear();
99  Unlock();
100 }
101 
103 //
105 
106 QString ServerSideScripting::SetResourceRootPath( const QString &path )
107 {
108  Lock();
109  QString sOrig = m_sResRootPath;
110 
111  m_sResRootPath = path;
112  Unlock();
113 
114  return sOrig;
115 }
116 
118 //
120 
122  const QMetaObject *pMetaObject,
123  QScriptEngine::FunctionSignature pFunction)
124 {
125  Lock();
126  QScriptValue ctor = m_engine.newFunction( pFunction );
127 
128  QScriptValue metaObject = m_engine.newQMetaObject( pMetaObject, ctor );
129  m_engine.globalObject().setProperty( sName, metaObject );
130  Unlock();
131 }
132 
134 //
136 
138 {
139  ScriptInfo *pInfo = nullptr;
140 
141  Lock();
142 
143  if ( m_mapScripts.contains( sFileName ) )
144  pInfo = m_mapScripts[ sFileName ];
145 
146  Unlock();
147 
148  return pInfo;
149 }
150 
152 //
154 
155 bool ServerSideScripting::EvaluatePage( QTextStream *pOutStream, const QString &sFileName,
156  HTTPRequest *pRequest, const QByteArray &cspToken)
157 {
158  try
159  {
160 
161  ScriptInfo *pInfo = nullptr;
162 
163  // ------------------------------------------------------------------
164  // See if page has already been loaded
165  // ------------------------------------------------------------------
166 
167  pInfo = GetLoadedScript( sFileName );
168 
169  // ------------------------------------------------------------------
170  // Load Script File and Create Function
171  // ------------------------------------------------------------------
172 
173  QFileInfo fileInfo ( sFileName );
174  QDateTime dtLastModified = fileInfo.lastModified();
175 
176  Lock();
177  if ((pInfo == nullptr) || (pInfo->m_dtTimeStamp != dtLastModified ))
178  {
179  QString sCode = CreateMethodFromFile( sFileName );
180 
181  QScriptValue func = m_engine.evaluate( sCode, sFileName );
182 
183  if ( m_engine.hasUncaughtException() )
184  {
185  LOG(VB_GENERAL, LOG_ERR,
186  QString("Uncaught exception loading QSP File: %1 - (line %2) %3")
187  .arg(sFileName)
188  .arg(m_engine.uncaughtExceptionLineNumber())
189  .arg(m_engine.uncaughtException().toString()));
190 
191  Unlock();
192  return false;
193  }
194 
195  if (pInfo != nullptr)
196  {
197  pInfo->m_oFunc = func;
198  pInfo->m_dtTimeStamp = dtLastModified;
199  }
200  else
201  {
202  pInfo = new ScriptInfo( func, dtLastModified );
203  m_mapScripts[ sFileName ] = pInfo;
204  }
205  }
206 
207  // ------------------------------------------------------------------
208  // Build array of arguments passed to script
209  // ------------------------------------------------------------------
210 
211  QStringMap mapParams = pRequest->m_mapParams;
212 
213  // Valid characters for object property names must contain only
214  // word characters and numbers, _ and $
215  // They must not start with a number - to simplify the regexp, we
216  // restrict the first character to the English alphabet
217  QRegExp validChars = QRegExp("^([a-zA-Z]|_|\\$)(\\w|\\$)+$");
218 
219  QVariantMap params;
220  QMap<QString, QString>::const_iterator it = mapParams.begin();
221  QString prevArrayName = "";
222  QVariantMap array;
223  for (; it != mapParams.end(); ++it)
224  {
225  const QString& key = it.key();
226  QVariant value = QVariant(it.value());
227 
228  // PHP Style parameter array
229  if (key.contains("["))
230  {
231  QString arrayName = key.section('[',0,0);
232  QString arrayKey = key.section('[',1,1);
233  arrayKey.chop(1); // Remove trailing ]
234  if (prevArrayName != arrayName) // New or different array
235  {
236  if (!array.isEmpty())
237  {
238  params.insert(prevArrayName, QVariant(array));
239  array.clear();
240  }
241  prevArrayName = arrayName;
242  }
243 
244  if (!validChars.exactMatch(arrayKey)) // Discard anything that isn't valid for now
245  continue;
246 
247  array.insert(arrayKey, value);
248 
249  if ((it + 1) != mapParams.end())
250  continue;
251  }
252 
253  if (!array.isEmpty())
254  {
255  params.insert(prevArrayName, QVariant(array));
256  array.clear();
257  }
258  // End Array handling
259 
260  if (!validChars.exactMatch(key)) // Discard anything that isn't valid for now
261  continue;
262 
263  params.insert(key, value);
264 
265  }
266 
267  // ------------------------------------------------------------------
268  // Build array of request headers
269  // ------------------------------------------------------------------
270 
271  QStringMap mapHeaders = pRequest->m_mapHeaders;
272 
273  QVariantMap requestHeaders;
274  for (it = mapHeaders.begin(); it != mapHeaders.end(); ++it)
275  {
276  QString key = it.key();
277  key = key.replace('-', '_'); // May be other valid chars in a request header that we need to replace
278  QVariant value = QVariant(it.value());
279 
280  if (!validChars.exactMatch(key)) // Discard anything that isn't valid for now
281  continue;
282 
283  requestHeaders.insert(key, value);
284  }
285 
286  // ------------------------------------------------------------------
287  // Build array of cookies
288  // ------------------------------------------------------------------
289 
290  QStringMap mapCookies = pRequest->m_mapCookies;
291 
292  QVariantMap requestCookies;
293  for (it = mapCookies.begin(); it != mapCookies.end(); ++it)
294  {
295  QString key = it.key();
296  key = key.replace('-', '_'); // May be other valid chars in a request header that we need to replace
297  QVariant value = QVariant(it.value());
298 
299  if (!validChars.exactMatch(key)) // Discard anything that isn't valid for now
300  continue;
301 
302  requestCookies.insert(key, value);
303  }
304 
305  // ------------------------------------------------------------------
306  // Build array of information from the server e.g. client IP
307  // See RFC 3875 - The Common Gateway Interface
308  // ------------------------------------------------------------------
309 
310  QVariantMap serverVars;
311  //serverVars.insert("QUERY_STRING", QVariant())
312  serverVars.insert("REQUEST_METHOD", QVariant(pRequest->GetRequestType()));
313  serverVars.insert("SCRIPT_NAME", QVariant(sFileName));
314  serverVars.insert("REMOTE_ADDR", QVariant(pRequest->GetPeerAddress()));
315  serverVars.insert("SERVER_NAME", QVariant(pRequest->GetHostName()));
316  serverVars.insert("SERVER_PORT", QVariant(pRequest->GetHostPort()));
317  serverVars.insert("SERVER_PROTOCOL", QVariant(pRequest->GetRequestProtocol()));
318  serverVars.insert("SERVER_SOFTWARE", QVariant(HttpServer::GetServerVersion()));
319 
320  QHostAddress clientIP = QHostAddress(pRequest->GetPeerAddress());
321  QHostAddress serverIP = QHostAddress(pRequest->GetHostAddress());
322  if (clientIP.protocol() == QAbstractSocket::IPv4Protocol)
323  {
324  serverVars.insert("IP_PROTOCOL", "IPv4");
325  }
326  else if (clientIP.protocol() == QAbstractSocket::IPv6Protocol)
327  {
328  serverVars.insert("IP_PROTOCOL", "IPv6");
329  }
330 
331  if (((clientIP.protocol() == QAbstractSocket::IPv4Protocol) &&
332  (clientIP.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
333  clientIP.isInSubnet(QHostAddress("192.168.0.0"), 16) ||
334  clientIP.isInSubnet(QHostAddress("10.0.0.0"), 8))) ||
335  ((clientIP.protocol() == QAbstractSocket::IPv6Protocol) &&
336  clientIP.isInSubnet(serverIP, 64))) // default subnet size is assumed to be /64
337  {
338  serverVars.insert("CLIENT_NETWORK", "local");
339  }
340  else
341  {
342  serverVars.insert("CLIENT_NETWORK", "remote");
343  }
344 
345  // ------------------------------------------------------------------
346  // User Session information
347  //
348  // SECURITY
349  // The session token and password digest are considered sensitive on
350  // unencrypted connections and therefore must never be included in the
351  // HTML. An intercepted session token or password digest can be used
352  // to login or hijack an existing session.
353  // ------------------------------------------------------------------
354  MythUserSession session = pRequest->m_userSession;
355  QVariantMap sessionVars;
356  sessionVars.insert("username", session.GetUserName());
357  sessionVars.insert("userid", session.GetUserId());
358  sessionVars.insert("created", session.GetSessionCreated());
359  sessionVars.insert("lastactive", session.GetSessionLastActive());
360  sessionVars.insert("expires", session.GetSessionExpires());
361 
362  // ------------------------------------------------------------------
363  // Add the arrays (objects) we've just created to the global scope
364  // They may be accessed as 'Server.REMOTE_ADDR'
365  // ------------------------------------------------------------------
366 
367  m_engine.globalObject().setProperty("Parameters",
368  m_engine.toScriptValue(params));
369  m_engine.globalObject().setProperty("RequestHeaders",
370  m_engine.toScriptValue(requestHeaders));
371  QVariantMap respHeaderMap;
372  m_engine.globalObject().setProperty("ResponseHeaders",
373  m_engine.toScriptValue(respHeaderMap));
374  m_engine.globalObject().setProperty("Server",
375  m_engine.toScriptValue(serverVars));
376  m_engine.globalObject().setProperty("Session",
377  m_engine.toScriptValue(sessionVars));
378  QScriptValue qsCspToken = m_engine.toScriptValue(cspToken);
379  m_engine.globalObject().setProperty("CSP_NONCE", qsCspToken);
380 
381 
382  // ------------------------------------------------------------------
383  // Execute function to render output
384  // ------------------------------------------------------------------
385 
386  OutputStream outStream( pOutStream );
387 
388  QScriptValueList args;
389  args << m_engine.newQObject( &outStream );
390  args << m_engine.toScriptValue(params);
391 
392  QScriptValue ret = pInfo->m_oFunc.call( QScriptValue(), args );
393 
394  if (ret.isError())
395  {
396  QScriptValue lineNo = ret.property( "lineNumber" );
397 
398  LOG(VB_GENERAL, LOG_ERR,
399  QString("Error calling QSP File: %1(%2) - %3")
400  .arg(sFileName)
401  .arg( lineNo.toInt32 () )
402  .arg( ret .toString() ));
403  Unlock();
404  return false;
405 
406  }
407 
408  if (m_engine.hasUncaughtException())
409  {
410  LOG(VB_GENERAL, LOG_ERR,
411  QString("Uncaught exception calling QSP File: %1(%2) - %3")
412  .arg(sFileName)
413  .arg(m_engine.uncaughtExceptionLineNumber() )
414  .arg(m_engine.uncaughtException().toString()));
415  Unlock();
416  return false;
417  }
418  Unlock();
419  }
420  catch(...)
421  {
422  LOG(VB_GENERAL, LOG_ERR,
423  QString("Exception while evaluating QSP File: %1") .arg(sFileName));
424 
425  Unlock();
426  return false;
427  }
428 
429  // Apply any custom headers defined by the script
430  QVariantMap responseHeaders;
431  responseHeaders = m_engine.fromScriptValue< QVariantMap >
432  (m_engine.globalObject().property("ResponseHeaders"));
433  QVariantMap::iterator it;
434  for (it = responseHeaders.begin(); it != responseHeaders.end(); ++it)
435  {
436  pRequest->SetResponseHeader(it.key(), it.value().toString(), true);
437  }
438 
439  return true;
440 }
441 
443 //
445 
446 QString ServerSideScripting::CreateMethodFromFile( const QString &sFileName ) const
447 {
448  bool bInCode = false;
449  QString sBuffer;
450  QTextStream sCode( &sBuffer );
451 
452  QFile scriptFile( sFileName );
453 
454  if (!scriptFile.open( QIODevice::ReadOnly ))
455  throw "Unable to open file";
456 
457  try
458  {
459  QTextStream stream( &scriptFile );
460  QString sTransBuffer;
461 
462  sCode << "(function( os, ARGS ) {\n";
463  sCode << "try {\n";
464 
465  while( !stream.atEnd() )
466  {
467  QString sLine = stream.readLine();
468 
469  bInCode = ProcessLine( sCode, sLine, bInCode, sTransBuffer );
470  }
471 
472  sCode << "} catch( err ) { return err; }\n";
473  sCode << "})";
474  }
475  catch(...)
476  {
477  LOG(VB_GENERAL, LOG_ERR,
478  QString("Exception while reading QSP File: %1") .arg(sFileName));
479  }
480 
481  scriptFile.close();
482 
483  sCode.flush();
484 
485  return sBuffer;
486 }
487 
489 //
491 
492 QString ServerSideScripting::ReadFileContents( const QString &sFileName ) const
493 {
494  QString sCode;
495  QFile scriptFile( sFileName );
496 
497  if (!scriptFile.open( QIODevice::ReadOnly ))
498  throw "Unable to open file";
499 
500  try
501  {
502  QTextStream stream( &scriptFile );
503 
504  sCode = stream.readAll();
505  }
506  catch(...)
507  {
508  LOG(VB_GENERAL, LOG_ERR,
509  QString("Exception while Reading File Contents File: %1") .arg(sFileName));
510  }
511 
512  scriptFile.close();
513 
514  return sCode;
515 }
516 
518 //
520 
521 bool ServerSideScripting::ProcessLine( QTextStream &sCode,
522  QString &sLine,
523  bool bInCode,
524  QString &sTransBuffer ) const
525 {
526  QString sLowerLine = sLine.toLower();
527 
528  if (!sTransBuffer.isEmpty())
529  {
530  int nEndTransPos = sLowerLine.indexOf("</i18n>");
531 
532  if (nEndTransPos == -1)
533  {
534  sTransBuffer.append(" ");
535  sTransBuffer.append(sLine);
536  return bInCode;
537  }
538 
539  if (nEndTransPos > 0)
540  sTransBuffer.append(" ");
541 
542  sTransBuffer.append(sLine.left(nEndTransPos).trimmed());
543  QString trStr =
544  QCoreApplication::translate("HtmlUI", sTransBuffer.trimmed().toLocal8Bit().data());
545  trStr.replace( '"', "\\\"" );
546  sCode << "os.write( \"" << trStr << "\" );\n";
547  sTransBuffer = "";
548 
549  if (nEndTransPos == (sLine.length() - 7))
550  return bInCode;
551 
552  sLine = sLine.right(sLine.length() - (nEndTransPos + 7));
553  }
554 
555  int nStartTransPos = sLowerLine.indexOf("<i18n>");
556  if (nStartTransPos != -1)
557  {
558  int nEndTransPos = sLowerLine.indexOf("</i18n>");
559  if (nEndTransPos != -1)
560  {
561  QString patStr = sLine.mid(nStartTransPos,
562  (nEndTransPos + 7 - nStartTransPos));
563  QString repStr = patStr.mid(6, patStr.length() - 13).trimmed();
564  sLine.replace(patStr, QCoreApplication::translate("HtmlUI", repStr.toLocal8Bit().data()));
565  return ProcessLine(sCode, sLine, bInCode, sTransBuffer);
566  }
567 
568  sTransBuffer = " ";
569  sTransBuffer.append(sLine.mid(nStartTransPos + 6).trimmed());
570  sLine = sLine.left(nStartTransPos);
571  }
572 
573  int nStartPos = 0;
574  int nEndPos = 0;
575  int nMatchPos = 0;
576  bool bMatchFound = false;
577 
578  QString sExpecting = bInCode ? "%>" : "<%";
579  bool bNewLine = !(sLine.startsWith( sExpecting ));
580 
581  while (nStartPos < sLine.length())
582  {
583  nEndPos = sLine.length() - 1;
584 
585  sExpecting = bInCode ? "%>" : "<%";
586  nMatchPos = sLine.indexOf( sExpecting, nStartPos );
587 
588  // ------------------------------------------------------------------
589  // If not found, Adjust to Save entire line
590  // ------------------------------------------------------------------
591 
592  if (nMatchPos < 0)
593  {
594  nMatchPos = nEndPos + 1;
595  bMatchFound = false;
596  }
597  else
598  bMatchFound = true;
599 
600  // ------------------------------------------------------------------
601  // Add Code or Text to Line
602  // ------------------------------------------------------------------
603 
604  QString sSegment = sLine.mid( nStartPos, nMatchPos - nStartPos );
605 
606  if ( !sSegment.isEmpty())
607  {
608  if (bInCode)
609  {
610  // Add Code
611 
612  if (sSegment.startsWith( "=" ))
613  {
614  // Evaluate statement and render results.
615 
616  sCode << "os.write( " << sSegment.mid( 1 ) << " ); "
617  << "\n";
618  }
619  else if (sSegment.startsWith( "import" ))
620  {
621  // Loads supplied path as script file.
622  //
623  // Syntax: import "/relative/path/to/script.js"
624  // - must be at start of line (no leading spaces)
625  //
626 
627  // Extract filename (remove quotes)
628 
629  QStringList sParts = sSegment.split( ' ', QString::SkipEmptyParts );
630 
631  if (sParts.length() > 1 )
632  {
633  QString sFileName = sParts[1].mid( 1, sParts[1].length() - 2 );
634 
635  QFileInfo oInfo( m_sResRootPath + sFileName );
636 
637  if (oInfo.exists())
638  {
639  sCode << ReadFileContents( oInfo.canonicalFilePath() )
640  << "\n";
641  }
642  else
643  LOG(VB_GENERAL, LOG_ERR,
644  QString("ServerSideScripting::ProcessLine 'import' - File not found: %1%2")
645  .arg(m_sResRootPath)
646  .arg(sFileName));
647  }
648  else
649  {
650  LOG(VB_GENERAL, LOG_ERR,
651  QString("ServerSideScripting::ProcessLine 'import' - Malformed [%1]")
652  .arg( sSegment ));
653  }
654 
655  }
656  else
657  sCode << sSegment << "\n";
658 
659  if (bMatchFound)
660  bInCode = false;
661  }
662  else
663  {
664  // Add Text
665 
666  sSegment.replace( '"', "\\\"" );
667 
668  sCode << "os.write( \"" << sSegment << "\" );\n";
669 
670  if (bMatchFound)
671  bInCode = true;
672  }
673  }
674  else
675  {
676  if (bMatchFound)
677  bInCode = !bInCode;
678  }
679 
680  nStartPos = nMatchPos + 2;
681  }
682 
683  if ((bNewLine) && !bInCode )
684  sCode << "os.writeln( \"\" );\n";
685 
686  return bInCode;
687 }
ScriptInfo * GetLoadedScript(const QString &sFileName)
virtual QString GetHostAddress()=0
MythUserSession m_userSession
Definition: httprequest.h:158
QString toString(MarkTypes type)
virtual quint16 GetHostPort()=0
virtual QString GetPeerAddress()=0
QScriptValue formatStr(QScriptContext *context, QScriptEngine *interpreter)
QMap< QString, QString > QStringMap
Definition: upnputil.h:40
QDateTime m_dtTimeStamp
bool ProcessLine(QTextStream &sCode, QString &sLine, bool bInCode, QString &sTransBuffer) const
void RegisterMetaObjectType(const QString &sName, const QMetaObject *pMetaObject, QScriptEngine::FunctionSignature pFunction)
const QDateTime GetSessionCreated() const
Definition: mythsession.h:41
uint GetUserId(void) const
Definition: mythsession.h:36
const QDateTime GetSessionLastActive() const
Definition: mythsession.h:42
QString CreateMethodFromFile(const QString &sFileName) const
void SetResponseHeader(const QString &sKey, const QString &sValue, bool replace=false)
const QDateTime GetSessionExpires() const
Definition: mythsession.h:43
QStringMap m_mapParams
Definition: httprequest.h:127
virtual QString GetHostName()
bool EvaluatePage(QTextStream *pOutStream, const QString &sFileName, HTTPRequest *pRequest, const QByteArray &cspToken)
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
QString ReadFileContents(const QString &sFileName) const
QScriptValue m_oFunc
QString GetRequestType() const
static QString GetServerVersion(void)
Definition: httpserver.cpp:292
const QString GetUserName(void) const
Definition: mythsession.h:35
QStringMap m_mapCookies
Definition: httprequest.h:129
QString GetRequestProtocol() const
QStringMap m_mapHeaders
Definition: httprequest.h:128
QString SetResourceRootPath(const QString &path)
QMap< QString, ScriptInfo * > m_mapScripts
QScriptEngineDebugger m_debugger