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