#include "fbgui.h"
#include "sysinfo.h"
#include "downloadmanager.h"
#include "javascriptinterface.h"
#include <log4cxx/logger.h>
#include "qlog4cxx.h"
using namespace log4cxx;
using namespace log4cxx::helpers;
LoggerPtr coreLogger(Logger::getLogger("fbgui.core"));
#include <iostream>
#include <QThread>
#include <QtWebKit>
QThread dmThread;
QString logFilePath("");
QString ipConfigFilePath("");
QString binPath("");
QUrl baseURL("");
QString downloadPath("");
int updateInterval = -1;
QString fileToTriggerURL("");
QString serialLocation("");
QString sessionID("");
bool sslSupport;
int debugMode = -1;
//-------------------------------------------------------------------------------------------
/**
* A constructor.
*
* The constructor of the fbgui class. It initializes the main objects
* which are needed while the program is running.
* The appearance of the webView is here also defined.
*
* @see JavascriptInterface
* @see DownloadManager
*/
fbgui::fbgui() {
}
fbgui::~fbgui() {
dmThread.quit();
}
/**
* init function.
*/
void fbgui::init() {
// start fbgui
LOG4CXX_DEBUG(coreLogger, "Initializing fbgui...");
if(sslSupport)
LOG4CXX_DEBUG(coreLogger, "SSL enabled.");
_watcher = new QFileSystemWatcher(this);
setupLayout();
createActions();
// initialize javascript interface
JavascriptInterface* jsi = new JavascriptInterface(
_webView->page()->mainFrame());
QObject::connect(jsi, SIGNAL(quitFbgui()), this, SLOT(close()));
QObject::connect(jsi, SIGNAL(shutDownClient()), this,
SLOT(performShutDown()));
QObject::connect(_webView->page()->mainFrame(), SIGNAL(
javaScriptWindowObjectCleared()), jsi, SLOT(attachToDOM()));
// initialize download manager
DownloadManager* dm = new DownloadManager();
QObject::connect(dm, SIGNAL(downloadInfo(const QString&, const double&)),
jsi, SLOT(downloadInfo(const QString&, const double&)));
QObject::connect(dm, SIGNAL(notify(const QString&)), jsi,
SLOT(notify(const QString&)));
QObject::connect(jsi, SIGNAL(requestFile(const QString&)), dm,
SLOT(downloadFile(const QString&)));
QObject::connect(
dm,
SIGNAL(updateProgress(const int&, const double&, const QString&)),
jsi,
SLOT(updateProgressBar(const int&, const double&, const QString&)));
QObject::connect(dm, SIGNAL(downloadQueueEmpty()), jsi, SLOT(
callbackOnFinished()));
QObject::connect(dm, SIGNAL(downloadQueueEmpty()), this, SLOT(loadSystem()));
// move download manager to its own thread
dm->moveToThread(&dmThread);
dmThread.start();
// show "waiting for internet" page until triggered.
// if (debugMode > -1) {
// _webView->load(QUrl("qrc:/html/preload-debug.html"));
// } else {
// _webView->load(QUrl("qrc:/html/preload.html"));
// }
// watcher is not needed anymore since we guarantee internet connection with the networkDiscovery.
// start watching for fileToTriggerURL
//watchForTrigger();
loadURL();
// set properties
setWindowTitle("fbgui");
setAttribute(Qt::WA_QuitOnClose, true);
setWindowFlags(Qt::FramelessWindowHint);
showFullScreen();
this->show();
}
//-------------------------------------------------------------------------------------------
// Layout / actions setup
//-------------------------------------------------------------------------------------------
/**
* This method sets the used Layout.
*
* This method sets the used Layout. Possible layout are:
* - browser mode: only the browser is visible
* - debug mode: the screen is divided into the browser and a debug
* out console
*/
void fbgui::setupLayout() {
// setup layout of the gui: debug split or browser
_webView = new QWebView(this);
if (debugMode == 1) {
// split main window in browser & debug console
createDebugConsole();
_splitter = new QSplitter(Qt::Vertical, this);
_splitter->addWidget(_webView);
_splitter->addWidget(_debugConsole);
setCentralWidget(_splitter);
} else
setCentralWidget(_webView);
}
//-------------------------------------------------------------------------------------------
/**
* This method enables a shortcut for closing the program.
* The shortcut itself is not configurable: CTRL + X
*/
void fbgui::createActions() {
_quit = new QAction(tr("&quit"), this);
_quit->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_X));
this->addAction(_quit);
connect(_quit, SIGNAL(triggered()), this, SLOT(close()));
}
//-------------------------------------------------------------------------------------------
// File system watching
//-------------------------------------------------------------------------------------------
/**
* This method sets a "watchdog" to a special file.
*
* This method sets a "watchdog" to a special file. If needed it creates the
* file which it has to watch over. It than connects a QFileSystemWatcher with
* this file. If changes happen to this file, the
* fbgui::checkForTrigger(const QString& dirname) method will be called.
*
*/
void fbgui::watchForTrigger() {
// check if fileToTriggerURL already exists
QFile file(fileToTriggerURL);
if (file.exists()) {
LOG4CXX_DEBUG(coreLogger, "[watcher] " << fileToTriggerURL << " found.");
// try to load URL
loadURL();
} else {
// create it
if (file.open(QIODevice::WriteOnly)) {
LOG4CXX_DEBUG(coreLogger, "Created: " << fileToTriggerURL);
file.close();
} else {
LOG4CXX_DEBUG(coreLogger, "Creation of " << fileToTriggerURL << " failed!");
LOG4CXX_DEBUG(coreLogger, "Exiting in 5 seconds...");
QTimer::singleShot(5000, this, SLOT(close()));
}
}
// watch the path to trigger file
LOG4CXX_DEBUG(coreLogger, "[watcher] Watching " << fileToTriggerURL);
_watcher->addPath(fileToTriggerURL);
//_watcher = new QFileSystemWatcher(QStringList(fileToTriggerURL, logFilePath), this);
QObject::connect(_watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(prepareURLLoad(const QString&)));
}
//-------------------------------------------------------------------------------------------
/**
* This method checks if the trigger was valid.
*
* This method checks if the trigger was valid. If yes,
* we have received an IP Address an can load the main screen.
* If not, something some error happened.
*
* @see fbgui::checkHost()
* @see fbgui::loadURL()
*/
void fbgui::prepareURLLoad(const QString& fileName) {
if (fileName == fileToTriggerURL) {
LOG4CXX_DEBUG(coreLogger, "[watcher] " << fileToTriggerURL << " changed!");
// disconnect _watcher, his job is done
LOG4CXX_DEBUG(coreLogger, "[watcher] disconnected.");
_watcher->disconnect(this);
_watcher->deleteLater();
// try to load URL
loadURL();
}
}
//-------------------------------------------------------------------------------------------
// Preparations for URL load
//-------------------------------------------------------------------------------------------
/**
* This method checks the existance of the host.
*
* This method checks if the host exists / can be found.
* The host is from the URL given through the configuration.
*/
bool fbgui::checkHost() const {
QHostInfo hostInfo = QHostInfo::fromName(baseURL.host());
if (hostInfo.error() != QHostInfo::NoError) {
LOG4CXX_DEBUG(coreLogger, "Lookup of " << baseURL.host() << "failed.");
LOG4CXX_DEBUG(coreLogger, "Host can not be reached.");
return false;
} else {
LOG4CXX_DEBUG(coreLogger, "Lookup of " << baseURL.host() << " succeeded.");
return true;
}
}
//-------------------------------------------------------------------------------------------
/**
* This method tries loads the URL.
*
* This method loads the main screen via an POST request. If also disconnects the watcher
* of the file, (Watcher is set in the fbgui::watchForTrigger() method).
* and generates the POST data body.
*
* @see fbgui::watchForTrigger()
* @see fbgui::generatePOSTData()
*/
void fbgui::loadURL() {
if (checkHost()) {
LOG4CXX_DEBUG(coreLogger, "Loading URL: " << baseURL.toString() << " ...");
// Generate POST identification data needed by PBS.
QByteArray postData = generatePOSTData();
_qnr = new QNetworkRequest(baseURL);
QList<QSslCertificate> certList = QSslCertificate::fromPath(QLatin1String("/usr/lib/ssl/openslx/CA/certs/openslx-cert.pem"));
QSslError error(QSslError::SelfSignedCertificateInChain, certList.at(0));
_expectedSslErrors.append(error);
registerCACertificates(certList);
//Connect webViews NetworkAccessManager to SSLErrorHandler SLOT
QObject::connect(_webView->page()->networkAccessManager(),
SIGNAL(finished(QNetworkReply*)),
this,
SLOT(httpErrorHandler(QNetworkReply*)));
//Connect webViews NetworkAccessManager to ErrorHandler SLOT
QObject::connect(_webView->page()->networkAccessManager(),
SIGNAL(sslErrors(QNetworkReply*, const QList<QSslError> & )),
this,
SLOT(sslErrorHandler(QNetworkReply*, const QList<QSslError> & )));
// show cursor again since user is about to interact.
//QWSServer::instance()->setCursorVisible(true); //TODO: ?enabled in original
_qnr->setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QObject::connect(_webView, SIGNAL(loadFinished(bool)), this, SLOT(loadURLDone(bool)));
_webView->load(*_qnr, QNetworkAccessManager::PostOperation, postData);
}
// TODO: error page if no host.
}
void fbgui::loadURLDone(bool success) {
// done contains the success of the loading: false / true
if (!success) {
LOG4CXX_DEBUG(coreLogger, "Loading failed. URL: " << _webView->url().toString());
LOG4CXX_DEBUG(coreLogger, "You can quit with CTRL + X ...");
// TODO handle failure properly...
} else {
LOG4CXX_DEBUG(coreLogger, "Loaded URL: " << _webView->url().toString());
//dumpCACertificates();
}
}
//Handles QNetworkReply SSL Errors
void fbgui::sslErrorHandler(QNetworkReply* reply, const QList<QSslError> & errlist) {
foreach (QSslError err, errlist)
LOG4CXX_DEBUG(coreLogger, "SSL Error: " << err.error());
// QSslConfiguration replySslConf = reply->sslConfiguration();
// foreach (QSslCertificate cert, replySslConf.peerCertificateChain())
// LOG4CXX_DEBUG(coreLogger,"Cert info: \n" << cert.toPem());
reply->ignoreSslErrors(_expectedSslErrors);
}
//Handles QNetworkReply Errors
void fbgui::httpErrorHandler(QNetworkReply* reply) {
if(reply->error() != QNetworkReply::NoError )
LOG4CXX_DEBUG(coreLogger, "HTTP Error: " << reply->errorString());
}
//test function to read Certificates
QSslCertificate fbgui::readCertificate(const QString& fileName){
QFile certFile(fileName);
Q_ASSERT(certFile.open(QIODevice::ReadOnly));
QByteArray certContent = certFile.readAll();
LOG4CXX_DEBUG(coreLogger,"Is cert valid: " << certContent);
QSslCertificate cert(certContent, QSsl::Pem);
return cert;
}
//Dump all Certificates in SSLConfiguration
void fbgui::dumpCACertificates(){
QSslConfiguration sslConfig = _qnr->sslConfiguration();
QList<QSslCertificate> caCerts = sslConfig.caCertificates();
foreach (QSslCertificate cert, caCerts){
LOG4CXX_DEBUG(coreLogger,"Cert info: \n" << cert.toPem());
}
}
//Saves CACertificates to SslConfiguration
void fbgui::registerCACertificates(const QList<QSslCertificate> & certificates){
foreach (QSslCertificate cert, certificates){
LOG4CXX_DEBUG(coreLogger,"Is cert valid: " << cert.isValid());
LOG4CXX_DEBUG(coreLogger,"Cert Issuer: " << cert.issuerInfo(QSslCertificate::CommonName));
LOG4CXX_DEBUG(coreLogger,"Cert Subject: " << cert.subjectInfo(QSslCertificate::CommonName));
}
QSslConfiguration sslConfig = _qnr->sslConfiguration();
QList<QSslCertificate> caCerts = sslConfig.caCertificates();
caCerts.append(certificates);
sslConfig.setCaCertificates(caCerts);
_qnr->setSslConfiguration(sslConfig);
}
//Saves PrivateKey to SslConfiguration
void fbgui::setLocalCertificate(QSslCertificate& cert){
LOG4CXX_DEBUG(coreLogger,"Is cert valid: " << cert.isValid());
LOG4CXX_DEBUG(coreLogger,"Cert info: " << cert.issuerInfo(QSslCertificate::Organization));
QSslConfiguration sslConfig = _qnr->sslConfiguration();
sslConfig.setLocalCertificate(cert);
_qnr->setSslConfiguration(sslConfig);
}
//-------------------------------------------------------------------------------------------
/**
* This method generates the POST data body.
*
* This method generates the POST data body. The body contains the
* MAC address, an hardwarehash and a specific serial number.
* The hardwarehash is a MD5 hash over the MAC address and the
* mainboard serial number.
* The specific serial number is set at the creation of the usb boot stick.
* This file has to be present on the directory specified in
* the configuration for this to work.
*
* @see SysInfo::getMACAddress()
* @see SysInfo::getMainboardSerial()
*/
QByteArray fbgui::generatePOSTData() {
LOG4CXX_DEBUG(coreLogger, "Generating POST data...");
// use MAC address as base data
SysInfo si;
QByteArray data(si.getInfo("mac").toUtf8());
// append mainboard serial to the mac address for more unique hardwarehash
QByteArray mbserial(si.getInfo("mbserial").toUtf8());
if (!mbserial.isEmpty())
data.append(mbserial);
else {
LOG4CXX_DEBUG(coreLogger, "Mainboard serial was empty. Not appending to base hash data.");
}
LOG4CXX_DEBUG(coreLogger, "[post] Hashing: " << data);
// generate MD5 hash of data
QByteArray hash = QCryptographicHash::hash(data, QCryptographicHash::Md5);
LOG4CXX_DEBUG(coreLogger, "[post] MD5 Hash: " << hash.toHex());
// fetch serial number from usb
QByteArray serial;
QFile file(serialLocation);
if (!file.open(QIODevice::ReadOnly)) {
LOG4CXX_DEBUG(coreLogger, "[post] No such file: " << file.fileName());
}
// everything ok, read data
serial = file.readAll();
file.close();
serial.chop(1); // chop EOF
LOG4CXX_DEBUG(coreLogger, "[post] Serial number is: " << serial);
// construct final byte array
QByteArray postData("mac=");
postData.append(si.getInfo("mac"));
postData.append("&hardwarehash=" + hash.toHex());
postData.append("&serialnumber=" + serial);
LOG4CXX_DEBUG(coreLogger, "[post] POST data: " << postData);
return postData;
}
//-------------------------------------------------------------------------------------------
// Shutdown / Reboot of the client
//-------------------------------------------------------------------------------------------
// TODO One function for reboot and shutdown, with parameter for the action.
// for example: doSystemCall(_REBOOT_);
/**
* This method performs the shutdown of the client.
*
* This method performs the shutdown of the client. It is triggered by the
* JavascriptInterface::shutDownClient() signal which will be emited in the
* JavascriptInterface::shutDown() method.
* This method writes the character 'o' in /proc/sysrq-trigger
* which will shutdown the computer immediatly.
* (See linux magic keys)
*
* @see JavascriptInterface::shutDownClient()
* @see JavascriptInterface::shutDown()
*/
void fbgui::performShutDown() {
QFile file("/proc/sysrq-trigger");
if (file.open(QIODevice::WriteOnly)) {
file.write("o");
file.close();
} else {
LOG4CXX_DEBUG(coreLogger, "Could not open /proc/sysrq-trigger");
}
}
//-------------------------------------------------------------------------------------------
/**
* This method performs the reboot of the client.
*
* This method performs the reboot of the client. It is triggered by the
* JavascriptInterface::rebootClient() signal which will be emited in the
* JavascriptInterface::reboot() method.
* This method writes the character 'b' in /proc/sysrq-trigger
* which will shutdown the computer immediatly.
* (See linux magic keys)
*
* @see JavascriptInterface::rebootClient()
* @see JavascriptInterface::reboot()
*/
void fbgui::performReboot() {
QFile file("/proc/sysrq-trigger");
if (file.open(QIODevice::WriteOnly)) {
file.write("b");
file.close();
} else {
LOG4CXX_DEBUG(coreLogger, "Could not open /proc/sysrq-trigger");
}
}
//-------------------------------------------------------------------------------------------
// Preparing Kernel Switch per kexec (initiating Stage 3)
//-------------------------------------------------------------------------------------------
void fbgui::loadSystem() {
//show loading system page.
//_webView->disconnect(this);
//QObject::connect(_webView, SIGNAL(loadFinished(bool)), this, SLOT(prepareKexec()));
_webView->load(QUrl("qrc:/html/loadsystem.html"));
QTimer::singleShot(1000, this, SLOT(prepareKexec()));
}
//-------------------------------------------------------------------------------------------
/**
* This method prepares kexec.
*
* The kernel command line file that should have been downloaded from the Preboot-Server
* and the ip config file (created by udhcpc) are merged into the final completed KCL.
*
* A process is then started to load the kernel, initramfs and kcl into kexec.
* The process tries to execute kexec -l with these parameters.
*
* If this succeeds, runKexec() is called
*
* @see fbgui::runKexec()
*
*/
void fbgui::prepareKexec() {
LOG4CXX_DEBUG(coreLogger, "Preparing kexec ...");
// try to read KCL file that was downloaded.
QFile file(downloadPath + "/kcl");
if (!file.open(QIODevice::ReadOnly)) {
LOG4CXX_DEBUG(coreLogger, "No such file: " << file.fileName());
}
// everything ok, read data.
QString kcl = file.readAll();
file.close();
LOG4CXX_DEBUG(coreLogger, "KCL from PBS: " << kcl);
// try to read ipconfig file generated by udhcpc.
file.setFileName("/tmp/ip_config_fbgui");
if (!file.open(QIODevice::ReadOnly)) {
LOG4CXX_DEBUG(coreLogger, "No such file: " << file.fileName());
}
// everything ok, read data.
QString ipConfig = file.readAll();
file.close();
LOG4CXX_DEBUG(coreLogger, "IP config: " << ipConfig);
// append ipConfig
kcl.append(" ip=");
kcl.append(ipConfig);
LOG4CXX_DEBUG(coreLogger, "Complete KCL: " << kcl);
// load the kernel + initramfs + append of kcl into kexec.
QProcess *process = new QProcess(this);
QString cmdline = "kexec -l " + downloadPath.toUtf8() + "/kernel --initrd="
+ downloadPath.toUtf8() + "/initramfs --append=\"" + kcl.toUtf8()
+ "\"";
LOG4CXX_DEBUG(coreLogger, "kexec cmdline: " << cmdline);
process->start(cmdline);
bool ret = process->waitForFinished();
if (!ret) {
LOG4CXX_DEBUG(coreLogger, "Failed to execute: " << cmdline);
LOG4CXX_DEBUG(coreLogger, "Exiting in 5 seconds...");
QTimer::singleShot(5000, this, SLOT(close()));
} else {
LOG4CXX_DEBUG(coreLogger, "Kexec load was successfull.");
if (debugMode < 0) {
// if process successfully finished, try to run kexec -e
runKexec();
} else {
LOG4CXX_DEBUG(coreLogger, "Skipping execution of kexec - open debug shell.");
LOG4CXX_DEBUG(coreLogger, "To start the system execute \"kexec -e\" in your shell.");
close();
}
}
}
//-------------------------------------------------------------------------------------------
/**
* This method tries to execute: kexec -e
*
* This method tries to execute: kexec -e
*
*/
void fbgui::runKexec() {
QProcess *process = new QProcess(this);
process->start("kexec -e");
if (!process->waitForStarted()) {
LOG4CXX_DEBUG(coreLogger, "Failed to execute: kexec -e");
LOG4CXX_DEBUG(coreLogger, "Exiting in 5 seconds...");
QTimer::singleShot(5000, this, SLOT(close()));
//TODO: Handle failure properly...
}
}
//-------------------------------------------------------------------------------------------
// Debug console setup / control
//-------------------------------------------------------------------------------------------
/**
* This method creates a debug console as a widget.
*
* It is basicly a QTextEdit widget as provided by QT's Framework.
* An action to toggle this widget is implemented (CTRL + D).
*
* @see fbgui::toggleDebugConsole()
*/
void fbgui::createDebugConsole() {
// create the debug console widget
_debugConsole = new QTextEdit(this);
_debugConsole->setWindowFlags(Qt::FramelessWindowHint);
// fanciness
QPalette pal;
pal.setColor(QPalette::Base, Qt::black);
_debugConsole->setPalette(pal);
_debugConsole->setTextColor(Qt::white);
// CTRL + D toggles debug window
_toggleDebugConsole = new QAction(tr("&toggleDebug"), this);
_toggleDebugConsole->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_D));
addAction(_toggleDebugConsole);
connect(_toggleDebugConsole, SIGNAL(triggered()),
this, SLOT(toggleDebugConsole()));
// read from log file
_logFile = new QFile(logFilePath);
_logFileIn = new QTextStream(_logFile);
if (!_logFile->open(QFile::ReadOnly | QFile::Text)) {
//do error
}
_debugConsole->setPlainText(_logFileIn->readAll());
_debugConsole->moveCursor(QTextCursor::End);
LOG4CXX_DEBUG(coreLogger, "Log file opened.");
// watch log file
_watcher->addPath(logFilePath);
QObject::connect(_watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(refreshDebugConsole(const QString&)));
}
//-------------------------------------------------------------------------------------------
void fbgui::refreshDebugConsole(const QString& fileName) {
if (fileName == logFilePath) {
while (!_logFileIn->atEnd()) {
_debugConsole->append(_logFileIn->readLine());
}
_debugConsole->moveCursor(QTextCursor::End);
}
}
//-------------------------------------------------------------------------------------------
/**
* This method toggles the debug console.
*
* Toggle the visibility of the debug console if the action _toggleDebugConsole is triggered.
*
* @see fbgui::createDebugConsole()
*/
void fbgui::toggleDebugConsole() {
(_debugConsole->isVisible()) ? _debugConsole->hide() : _debugConsole->show();
}