diff --git a/src/server/AuthorizationManager.cpp b/src/server/AuthorizationManager.cpp new file mode 100644 index 0000000..5d1a0db --- /dev/null +++ b/src/server/AuthorizationManager.cpp @@ -0,0 +1,203 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include "AuthorizationManager.h" +#include +#include +#include + +// Stuff for PAM to work +#include +#include +#include +#include +#include +#include +#include +#include + +//Internal defines +#define TIMEOUTSECS 900 // (15 minutes) time before a token becomes invalid +#define AUTHCHARS QString("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") +#define TOKENLENGTH 20 + +AuthorizationManager::AuthorizationManager(){ + HASH.clear(); + //initialize the random number generator (need to generate auth tokens) + qsrand(QDateTime::currentMSecsSinceEpoch()); +} +AuthorizationManager::~AuthorizationManager(){ + +} + +// == Token Interaction functions == +void AuthorizationManager::clearAuth(QString token){ + //clear an authorization token + if(HASH.contains(token)){ HASH.remove(token); } +} + +bool AuthorizationManager::checkAuth(QString token){ + //see if the given token is valid + bool ok = false; + if(HASH.contains(token)){ + //Also verify that the token has not timed out + ok = (HASH[token] > QDateTime::currentDateTime()); + if(ok){ HASH.insert(token, QDateTime::currentDateTime().addSecs(TIMEOUTSECS)); } //valid - bump the timestamp + } + return ok; +} + +int AuthorizationManager::checkAuthTimeoutSecs(QString token){ + //Return the number of seconds that a token is valid for + if(!HASH.contains(token)){ return 0; } //invalid token + return QDateTime::currentDateTime().secsTo( HASH[token] ); +} + + +// == Token Generation functions +QString AuthorizationManager::LoginUP(bool localhost, QString user, QString pass){ + //Login w/ username & password + bool ok = false; + //First check that the user is valid on the system and part of the operator group + if(user!="root"){ + if(!getUserGroups(user).contains("operator")){ return ""; } //invalid user - needs to be part of operator group + } + //qDebug() << "Check username/password" << user << pass; + //Need to run the full username/password through PAM + ok = pam_checkPW(user,pass); + + qDebug() << "User Login Attempt:" << user << " Success:" << ok << " Local Login:" << localhost; + if(!ok){ return ""; } //invalid login + else{ return generateNewToken(); } //valid login - generate a new token for it +} + +QString AuthorizationManager::LoginService(bool localhost, QString service){ + //Login a particular automated service + qDebug() << "Service Login Attempt:" << service << " Success:" << localhost; + if(!localhost){ return ""; } //invalid - services must be local for access + //Check that the service is valid on the system + // -- TO-DO + + //Now generate a new token and send it back + return generateNewToken(); +} + +// ========================= +// PRIVATE +// ========================= +QString AuthorizationManager::generateNewToken(){ + QString tok; + for(int i=0; i July 2015 +// ================================= +#ifndef _PCBSD_REST_AUTHORIZATION_MANAGER_H +#define _PCBSD_REST_AUTHORIZATION_MANAGER_H + +#include +#include +#include + +class AuthorizationManager{ +public: + AuthorizationManager(); + ~AuthorizationManager(); + + // == Token Interaction functions == + void clearAuth(QString token); //clear an authorization token + bool checkAuth(QString token); //see if the given token is valid + int checkAuthTimeoutSecs(QString token); //Return the number of seconds that a token is valid for + + // == Token Generation functions + QString LoginUP(bool localhost, QString user, QString pass); //Login w/ username & password + QString LoginService(bool localhost, QString service); //Login a particular automated service + +private: + QHash HASH; + QString generateNewToken(); + QStringList getUserGroups(QString user); + + //PAM login/check files + bool pam_checkPW(QString user, QString pass); + void pam_logFailure(int ret); +}; + +#endif diff --git a/src/server/RestStructs.h b/src/server/RestStructs.h new file mode 100644 index 0000000..f371c58 --- /dev/null +++ b/src/server/RestStructs.h @@ -0,0 +1,105 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#ifndef _PCBSD_REST_SERVER_REST_STRUCTS_H +#define _PCBSD_REST_SERVER_REST_STRUCTS_H +#include +#include +#include + +#define CurHttpVersion QString("HTTP/1.1") + +//NOTE: The input structure parsing assumes a JSON input body +//NOTE: Common VERB's are: +/* GET - Read a resource (no changes made) + PUT - Insert/update a resource (makes changes - nothing automatically assigned for a new resource) + POST - Insert/update a resource (makes changes - automatically assigns data for new resource as necessary) + DELETE - Remove a resource (makes changes) + OPTIONS - List the allowed options on a resource (no changes made) + HEAD - List the response headers only (no changes made) +*/ +class RestInputStruct{ +public: + QString VERB, URI, HTTPVERSION; + QStringList Header; + QString Body; + + RestInputStruct(QString message){ + HTTPVERSION = CurHttpVersion; + if(!message.startsWith("{")){ + Header = message.section("\n{",0,0).split("\n"); + } + if(!Header.isEmpty()){ + QString line = Header.takeFirst(); //The first line is special (not a generic header) + VERB = line.section(" ",0,0); + URI = line.section(" ",1,1); + HTTPVERSION = line.section(" ",2,2); + Body = message.remove(Header.join("\n")+"\n"); //chop the headers off the front + }else{ + //simple bypass for any non-REST inputs - just have it go straight to JSON parsing + VERB = "GET"; + URI = "/syscache"; + Body = message; + } + } + ~RestInputStruct(){} + +}; + +class RestOutputStruct{ +public: + enum ExitCode{OK, CREATED, ACCEPTED, NOCONTENT, RESETCONTENT, PARTIALCONTENT, PROCESSING, BADREQUEST, UNAUTHORIZED, FORBIDDEN, NOTFOUND }; + QString HTTPVERSION; + QStringList Header; + ExitCode CODE; + QString Body; + + RestOutputStruct(){ + HTTPVERSION = CurHttpVersion; + CODE = BADREQUEST; //default exit code + } + ~RestOutputStruct(){} + + QString assembleMessage(){ + /* JUST OUTPUT RAW JSON - DISABLE REST FOR THE MOMENT + QStringList headers; + QString firstline = HTTPVERSION; + switch(CODE){ + case PROCESSING: + firstline.append(" 102 Processing"); break; + case OK: + firstline.append(" 200 OK"); break; + case CREATED: + firstline.append(" 201 Created"); break; + case ACCEPTED: + firstline.append(" 202 Accepted"); break; + case NOCONTENT: + firstline.append(" 204 No Content"); break; + case RESETCONTENT: + firstline.append(" 205 Reset Content"); break; + case PARTIALCONTENT: + firstline.append(" 206 Partial Content"); break; + case BADREQUEST: + firstline.append(" 400 Bad Request"); break; + case UNAUTHORIZED: + firstline.append(" 401 Unauthorized"); break; + case FORBIDDEN: + firstline.append(" 403 Forbidden"); break; + case NOTFOUND: + firstline.append(" 404 Not Found"); break; + } + headers << firstline; + headers << "Date: "+QDateTime::currentDateTime().toString(Qt::ISODate); + //Add other headers here as necessary + if(!Header.isEmpty()){ headers << Header; } + //Now add the body of the return + if(!Body.isEmpty()){ headers << "Content-Length: "+QString::number(Body.length()); } + headers << Body; + return headers.join("\n");*/ + return Body; + } +}; + +#endif diff --git a/src/server/Syscache_websocket_examples.txt b/src/server/Syscache_websocket_examples.txt new file mode 100644 index 0000000..475db79 --- /dev/null +++ b/src/server/Syscache_websocket_examples.txt @@ -0,0 +1,359 @@ +Example syscache calls through the websocket interface: +For up-to-date DB request options: send the "help [pkg/pbi/jail/search]" queries to have the syscache daemon return all the various options it currently supports. This document covers most of the common queries/replies though. +Note: Whenever "" is used in a query below, that can either be replaced by "#system" to probe the local system, or a jail ID for looking within a particular jail on the system. + +================= + - Authentication protocols + ================= +Once a websocket connection is made to the server, the client needs to authenticate itself to obtain access to the syscache service. There are a couple of possible methods for authentication: +-JSON Request - user/password login +{ +"namespace" : "rpc", +"name" : "auth", +"id" : "sampleID", +"args" : { + "username" : "myuser", + "password" : "mypassword" + } +} + +-JSON Request - pre-saved token authentication (note that a token is invalidated after 5 minutes of user inactivity) +{ +"namespace" : "rpc", +"name" : "auth_token", +"id" : "sampleID", +"args" : { + "token" : "MySavedAuthToken" + } +} + +-JSON Reply (valid authentication) + NOTE: The first element of the "args" array is the authentication token for use later as necessary, while the second element is the number of seconds for which that token is valid (reset after every successful communication with the websocket) - in this case it is set to 5 minutes of inactivity before the token is invalidated. Also note: the websocket server is currently set to close any connection to a client after 10 minutes of inactivity. +{ + "args": [ + "SampleAuthenticationToken", + 300 + ], + "id": "sampleID", + "name": "response", + "namespace": "rpc" +} + +-JSON Reply (invalid authentication - this may also happen for any type of system request if the user session timed out due to inactivity) +{ + "args": { + "code": 401, + "message": "Unauthorized" + }, + "id": "sampleID", + "name": "error", + "namespace": "rpc" +} + +-JSON Request - Clear the current pre-saved authentication token (such as signing out) +{ +"namespace" : "rpc", +"name" : "auth_clear", +"id" : "sampleID", +"args" : "junk argument" +} + +================================== + - Event notifications +================================== +The client may subscribe to event notifications as well (per-connection) + +-JSON Request - Subscribe to "dispatcher" events +{ +"namespace" : "events", +"name" : "subscribe", +"id" : "sampleID", +"args" : ["dispatcher"] +} + +-JSON Request - Unsubscribe to "dispatcher" events +{ +"namespace" : "events", +"name" : "unsubscribe", +"id" : "sampleID", +"args" : ["dispatcher"] +} + +-JSON Reply - a "dispatcher" event has occured +{ +"namespace" : "events", +"name" : "event", +"id" : "", +"args" : { + "name" : "dispatcher", + "args" : "} + pkgupdate {__system__|} + service {start|stop|restart} {servicetag} {servicerc} {__system__|} + getcfg {pbicdir} {__system__|} {key} + setcfg {pbicdir} {__system__|} {key} {value} + donecfg {pbicdir} {__system__|} + daemon + status + results + log {hash} + +=================================== + - General syscache system information/summaries + =================================== +For a query of the syscache information daemon, the "name" field of the input JSON object needs to be set to "syscache". +Note: The "app-summary" and "cage-summary" options are specifically designed for getting enough information for lots of small app icons in fewer syscache requests. +The "app-summary" return array is: [pkg origin, name, version, icon path, rating (out of 5), type, comment, config dir, isInstalled, canRemove]. +The "cage-summary" return array is: [origin, name, icon, architecture, FreeBSD version]. +List of possible input queries for general system information: +startsync: Manually start a system information sync (usually unnecessary) +needsreboot: [true/false] Check if the system needs to reboot to finish updates +isupdating: [true/false] Check if the system is currently performing updates +hasupdates: [true/false] Check if system updates are available +updatelog: Raw text output from the check for system updates +hasmajorupdates: [true/false] Check if FreeBSD system updates are available +majorupdatelog: Details about the major update(s) +hassecurityupdates: [true/false] Check if FreeBSD security updates are available +securityupdatelog: Details about any security update(s) +haspcbsdupdates: [true/false] Check if any PC-BSD hotfixes are available +pcbsdupdatelog: Details about any PC-BSD hotfixes + app-summary : Summary of info for an application +cage-summary : Summary of info for a PBI cage + +-JSON Request +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["needsreboot", "hasupdates", "updatelog", "#system app-summary mail/thunderbird", "cage-summary multimedia/plexmediaserver"] +} + +-JSON Reply +{ + "args": { + "#system app-summary mail/thunderbird": [ + "mail/thunderbird", + "Thunderbird", + "38.2.0_1", + "/var/db/pbi/index/mail/thunderbird/icon.png", + "5.00", + "Graphical", + "Mozilla Thunderbird is standalone mail and news that stands above ", + "/var/db/pbi/index/mail/thunderbird", + "true", + "true" + ], + "cage-summary multimedia/plexmediaserver": [ + "multimedia/plexmediaserver", + "Plex Media Server", + "/var/db/pbi/cage-index/multimedia/plexmediaserver/icon.png", + "amd64", + "10.1-RELEASE" + ], + "hasupdates": "false", + "needsreboot": "false", + "updatelog": "Checking for FreeBSD updates...
Your system is up to date!" + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} + +====================== + - PBI database queries/examples + ====================== +List Queries: "pbi list " where can be: "[all/server/graphical/text]apps", "[all/server/graphical/text]cats", or "cages" +App Queries: "pbi app " where can be: "author", "category", "confdir", "dependencies", "origin", "plugins, "rating", "relatedapps", "screenshots", "type", "tags", "comment", "description", "license", "maintainer", "name", "options", or "website" +Cage Queries: "pbi cage " where can be: "icon", "name", "description", "arch" fbsdver", "git", "gitbranch", "screenshots", "tags", "website" +Category Queries: "pbi cat + +-JSON Query +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["pbi list graphicalapps", "pbi list cages", "pbi app www/firefox author", "pbi app www/firefox category", "pbi list graphicalcats" ] +} + +-JSON Reply +{ + "args": { + "pbi app www/firefox author": "Mozilla", + "pbi app www/firefox category": "Web", + "pbi list cages": [ + "archivers/elephantdrive", + "multimedia/plexmediaserver" + ], + "pbi list graphicalapps": [ + "math/R", + "www/WebMagick", + "editors/abiword", + "audio/abraca", + (SHORTENED FOR BREVITY - THIS IS USUALLY QUITE LONG) + "x11/zenity", + "security/zenmap", + "games/zephulor", + "www/zope213" + ], + "pbi list graphicalcats": [ + "accessibility", + "archivers", + "astro", + "audio", + (SHORTENED FOR BREVITY - THIS IS USUALLY QUITE LONG) + "x11-themes", + "x11-toolkits", + "x11-wm" + ] + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} + +======================== +- PBI Category information retrieval +======================== +-JSON Query +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["pbi cat www name", "pbi cat www icon", "pbi cat www comment", "pbi cat www origin" ] +} + +-JSON Reply +{ + "args": { + "pbi cat www comment": "Web browsers, and other applications used for the web such as RSS readers", + "pbi cat www icon": "/var/db/pbi/index/PBI-cat-icons/www.png", + "pbi cat www name": "Web", + "pbi cat www origin": "www" + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} + +============== +- PBI cage examples +============== +DB Request format: "pbi cage " +Possible : "icon", "name", "description", "arch", "fbsdver", "git", "gitbranch", "screenshots", "tags", "website" +-JSON Query +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["pbi cage multimedia/plexmediaserver tags", "pbi cage multimedia/plexmediaserver website", "pbi cage multimedia/plexmediaserver description", "pbi cage multimedia/plexmediaserver name"] +} + +-JSON Reply +{ + "args": { + "pbi cage multimedia/plexmediaserver description": "Plex stores all of your audio, video, and photo files in your free Plex Media Server so you can access them from all your devices and stream from anywhere.", + "pbi cage multimedia/plexmediaserver name": "Plex Media Server", + "pbi cage multimedia/plexmediaserver tags": "streaming, multimedia, server", + "pbi cage multimedia/plexmediaserver website": "https://plex.tv" + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} + +================== +-PKG Database Information +================== +General Queries: "pkg " where can be: "remotelist", "installedlist", "hasupdates" (true/false returned), or "updatemessage". +Individual pkg queries: "pkg " +Note: "local" is used for installed applications, while "remote" is for information available on the global repository (and might not match what is currently installed) + may be: "origin", "name", "version", "maintainer", "comment", "description", "website", "size", "arch", "message", "dependencies", "rdependencies", "categories", "options", "license" +For "local" pkgs, there are some additional options: "timestamp", "isOrphan", "isLocked", "files", "users", and "groups" + +-JSON Query +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["pkg #system installedlist", "pkg #system local mail/thunderbird version", "pkg #system remote mail/thunderbird version", "pkg #system local mail/thunderbird files" ] +} + +-JSON Reply +{ + "args": { + "pkg #system installedlist": [ + "graphics/ImageMagick", + "devel/ORBit2", + "graphics/OpenEXR", + (SHORTENED FOR BREVITY - THIS GETS QUITE LONG) + "archivers/zip", + "devel/zziplib" + ], + "pkg #system local mail/thunderbird files": [ + "/usr/local/bin/thunderbird", + "/usr/local/lib/thunderbird/application.ini", + "/usr/local/lib/thunderbird/blocklist.xml", + "/usr/local/lib/thunderbird/chrome.manifest", + (SHORTENED FOR BREVITY - THIS GETS QUITE LONG) + "/usr/local/share/applications/thunderbird.desktop", + "/usr/local/share/pixmaps/thunderbird.png" + ], + "pkg #system local mail/thunderbird version": "38.2.0_1", + "pkg #system remote mail/thunderbird version": "38.2.0_1" + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} + +============== + - Search Capabilities + ============== +Query Syntax: " search [/] [result minimum] +The search always returns an array of , organized in order of priority (first element is highest priority, last element is the lowest priority). +"pbi" probes the PBI database of end-user applications (independent of what is actually available/installed), whereas "pkg" searches all available/installed packages (whether they are designed for end-users or not). +The "" option may only be used for pkg searches, and corresponds to normal syntax ("#system" or jail ID). If it is not supplied, it assumes a search for the local system (#system). +The "" option may only be used for PBI searches to restrict the type of application being looked for, and may be: "all" "[not]graphical", "[not]server", and "[not]text". The default value is "all" (if that option is not supplied). +The "result minimum" is the number of results the search should try to return (10 by default). The search is done by putting all the apps into various "priority groups", and only the highest-priority groups which result in the minimum desired results will be used. For example: if the search comes up with grouping of 3-highest priority, 5-medium priority, and 20-low priority, then a minimum search of 2 will only return the "highest" priority group, a minimum search of 4 will return the highest and medium priority groups, and a minimum of 9+ will result in all the groups getting returned. + +-JSON Query +{ +"namespace" : "rpc", +"name" : "syscache", +"id" : "someUniqueID", +"args" : ["pbi search \"thun\" ", "pbi search \"thun\" text", "pkg search \"thun\""] +} + +-JSON Reply +{ + "args": { + "pbi search \"thun\" ": [ + "x11-fm/thunar", + "mail/thunderbird", + "www/thundercache", + "www/thundersnarf", + "x11/alltray", + "deskutils/gbirthday", + "audio/gtkpod", + "www/libxul" + ], + "pbi search \"thun\" text": "www/thundersnarf", + "pkg search \"thun\"": " " + }, + "id": "someUniqueID", + "name": "response", + "namespace": "rpc" +} diff --git a/src/server/WebBackend.cpp b/src/server/WebBackend.cpp new file mode 100644 index 0000000..66801a5 --- /dev/null +++ b/src/server/WebBackend.cpp @@ -0,0 +1,68 @@ +// =============================== +// PC-BSD REST API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore DEC 2015 +// ================================= +#include + +//sysadm library interface classes +#include +#include + +#include "syscache-client.h" +#include "dispatcher-client.h" + +#define DEBUG 0 +#define SCLISTDELIM QString("::::") //SysCache List Delimiter + +void WebSocket::EvaluateBackendRequest(QString name, const QJsonValue args, QJsonObject *out){ + QJsonObject obj; //output object + if(args.isObject()){ + //For the moment: all arguments are full syscache DB calls - no special ones + QStringList reqs = args.toObject().keys(); + if(!reqs.isEmpty()){ + if(DEBUG){ qDebug() << "Parsing Inputs:" << reqs; } + for(int r=0; rinsert(req, QJsonValue(values.join("")) ); } + else{ + //This is an array of outputs + QJsonArray arr; + for(int i=0; iinsert(req,arr); + } + } + } //end of special "request" objects + }else if(args.isArray()){ + QStringList inputs = JsonArrayToStringList(args.toArray()); + if(DEBUG){ qDebug() << "Parsing Array inputs:" << inputs; } + QStringList values; + if(name.toLower()=="syscache"){values = SysCacheClient::parseInputs( inputs ); } + else if(name.toLower()=="dispatcher"){values = DispatcherClient::parseInputs( inputs , AUTHSYSTEM); } + if(DEBUG){ qDebug() << " - Returns:" << values; } + for(int i=0; iinsert(inputs[i],arr); + }else{ + out->insert(inputs[i],values[i]); + } + } + } //end array of inputs + +} \ No newline at end of file diff --git a/src/server/WebBackend.h b/src/server/WebBackend.h new file mode 100644 index 0000000..328e541 --- /dev/null +++ b/src/server/WebBackend.h @@ -0,0 +1,17 @@ +// =============================== +// PC-BSD REST API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore DEC 2015 +// ================================= +#ifndef _SYSADM_WEBSERVER_BACKEND_CLASS_H +#define _SYSADM_WEBSERVER_BACKEND_CLASS_H + +//sysadm library interface classes +#include +#include + +#include "syscache-client.h" + + + +#endif diff --git a/src/server/WebServer.cpp b/src/server/WebServer.cpp new file mode 100644 index 0000000..ee6f1df --- /dev/null +++ b/src/server/WebServer.cpp @@ -0,0 +1,175 @@ +// =============================== +// PC-BSD REST API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include "WebServer.h" + +#include +#include +#include +#include +#include + +#define DEBUG 0 + +#define PORTNUMBER 12142 + +#define APPCAFEWORKING QString("/var/tmp/appcafe/dispatch-queue.working") + +//======================= +// PUBLIC +//======================= +WebServer::WebServer() : QWebSocketServer("syscache-webclient", QWebSocketServer::NonSecureMode){ + //Setup all the various settings + //Any SSL changes + /*QSslConfiguration ssl = this->sslConfiguration(); + ssl.setProtocol(QSsl::SecureProtocols); + this->setSslConfiguration(ssl);*/ + AUTH = new AuthorizationManager(); + watcher = new QFileSystemWatcher(this); + + //Setup Connections + connect(this, SIGNAL(closed()), this, SLOT(ServerClosed()) ); + connect(this, SIGNAL(serverError(QWebSocketProtocol::CloseCode)), this, SLOT(ServerError(QWebSocketProtocol::CloseCode)) ); + connect(this, SIGNAL(newConnection()), this, SLOT(NewSocketConnection()) ); + connect(this, SIGNAL(acceptError(QAbstractSocket::SocketError)), this, SLOT(NewConnectError(QAbstractSocket::SocketError)) ); + connect(this, SIGNAL(originAuthenticationRequired(QWebSocketCorsAuthenticator*)), this, SLOT(OriginAuthRequired(QWebSocketCorsAuthenticator*)) ); + connect(this, SIGNAL(peerVerifyError(const QSslError&)), this, SLOT(PeerVerifyError(const QSslError&)) ); + connect(this, SIGNAL(sslErrors(const QList&)), this, SLOT(SslErrors(const QList&)) ); + connect(watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(WatcherUpdate(QString)) ); + connect(watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(WatcherUpdate(QString)) ); +} + +WebServer::~WebServer(){ + delete AUTH; +} + +bool WebServer::startServer(){ + bool ok = this->listen(QHostAddress::Any, PORTNUMBER); + if(ok){ + QCoreApplication::processEvents(); + qDebug() << "Server Started:" << QDateTime::currentDateTime().toString(Qt::ISODate); + qDebug() << " Name:" << this->serverName() << "Port:" << this->serverPort(); + qDebug() << " URL:" << this->serverUrl().toString() << "Remote Address:" << this->serverAddress().toString(); + if(!QFile::exists(APPCAFEWORKING)){ QProcess::execute("touch "+APPCAFEWORKING); } + qDebug() << " Dispatcher Events:" << APPCAFEWORKING; + watcher->addPath(APPCAFEWORKING); + WatcherUpdate(APPCAFEWORKING); //load it initially + }else{ qCritical() << "Could not start server - exiting..."; } + return ok; +} + +void WebServer::stopServer(){ + this->close(); +} + +//=================== +// PRIVATE +//=================== +QString WebServer::generateID(){ + int id = 0; + for(int i=0; iID().toInt()>=id){ id = OpenSockets[i]->ID().toInt()+1; } + } + return QString::number(id); +} + +QString WebServer::readFile(QString path){ + QFile file(path); + if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){ return ""; } + QTextStream in(&file); + QString contents = in.readAll(); + file.close(); + if(contents.endsWith("\n")){ contents.chop(1); } + return contents; +} + +//======================= +// PRIVATE SLOTS +//======================= +// Overall Server signals +void WebServer::ServerClosed(){ + qDebug() << "Server Closed:" << QDateTime::currentDateTime().toString(Qt::ISODate); + QCoreApplication::exit(0); +} + +void WebServer::ServerError(QWebSocketProtocol::CloseCode code){ + qWarning() << "Server Error["+QString::number(code)+"]:" << this->errorString(); +} + +// New Connection Signals +void WebServer::NewSocketConnection(){ + if(!this->hasPendingConnections()){ return; } + qDebug() << "New Socket Connection"; + //if(idletimer->isActive()){ idletimer->stop(); } + QWebSocket *csock = this->nextPendingConnection(); + if(csock == 0){ qWarning() << " - new connection invalid, skipping..."; QTimer::singleShot(10, this, SLOT(NewSocketConnection())); return; } + qDebug() << " - Accepting connection:" << csock->origin(); + WebSocket *sock = new WebSocket(csock, generateID(), AUTH); + connect(sock, SIGNAL(SocketClosed(QString)), this, SLOT(SocketClosed(QString)) ); + connect(this, SIGNAL(DispatchStatusUpdate(QString)), sock, SLOT(AppCafeStatusUpdate(QString)) ); + sock->setLastDispatch(lastDispatch); //make sure this socket is aware of the latest notification + OpenSockets << sock; +} + +void WebServer::NewConnectError(QAbstractSocket::SocketError err){ + //if(csock!=0){ + //qWarning() << "New Connection Error["+QString::number(err)+"]:" << csock->errorString(); + //csock->close(); + //}else{ + qWarning() << "New Connection Error["+QString::number(err)+"]:" << this->errorString(); + //} + //csock = 0; //remove the current socket + QTimer::singleShot(0,this, SLOT(NewSocketConnection()) ); //check for a new connection + +} + +// SSL/Authentication Signals +void WebServer::OriginAuthRequired(QWebSocketCorsAuthenticator *auth){ + qDebug() << "Origin Auth Required:" << auth->origin(); + //if(auth->origin() == this->serverAddress().toString()){ + // TO-DO: Provide some kind of address filtering routine for which to accept/reject + qDebug() << " - Allowed"; + auth->setAllowed(true); + //}else{ + //qDebug() << " - Not Allowed"; + //auth->setAllowed(false); + //} + +} + +void WebServer::PeerVerifyError(const QSslError &err){ + qDebug() << "Peer Verification Error:" << err.errorString(); + +} + +void WebServer::SslErrors(const QList &list){ + qWarning() << "SSL Errors:"; + for(int i=0; iID()==ID){ delete OpenSockets.takeAt(i); break; } + } + QTimer::singleShot(0,this, SLOT(NewSocketConnection()) ); //check for a new connection +} + +void WebServer::WatcherUpdate(QString path){ + if(path==APPCAFEWORKING){ + //Read the file contents + QString stat = readFile(APPCAFEWORKING); + if(stat.simplified().isEmpty()){ stat = "idle"; } + qDebug() << "Dispatcher Update:" << stat; + lastDispatch = stat; //save for later + //Forward those contents on to the currently-open sockets + emit DispatchStatusUpdate(stat); + } + //Make sure this file/dir is not removed from the watcher + if(!watcher->files().contains(path) && !watcher->directories().contains(path)){ + watcher->addPath(path); //re-add it to the watcher. This happens when the file is removed/re-created instead of just overwritten + } +} \ No newline at end of file diff --git a/src/server/WebServer.h b/src/server/WebServer.h new file mode 100644 index 0000000..3787c8f --- /dev/null +++ b/src/server/WebServer.h @@ -0,0 +1,67 @@ +// =============================== +// PC-BSD REST API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#ifndef _PCBSD_REST_WEB_SERVER_H +#define _PCBSD_REST_WEB_SERVER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include //for better syntax of qDebug() / qWarning() / qCritical() / qFatal() + +#include "WebSocket.h" +#include "AuthorizationManager.h" + +class WebServer : public QWebSocketServer{ + Q_OBJECT +public: + WebServer(); + ~WebServer(); + + bool startServer(); + +public slots: + void stopServer(); + +private: + QList OpenSockets; + AuthorizationManager *AUTH; + QFileSystemWatcher *watcher; + QString lastDispatch; + + QString generateID(); //generate a new ID for a socket + QString readFile(QString path); + +private slots: + // Overall Server signals + void ServerClosed(); //closed() signal + void ServerError(QWebSocketProtocol::CloseCode); //serverError() signal + + // New Connection Signals + void NewSocketConnection(); //newConnection() signal + void NewConnectError(QAbstractSocket::SocketError); //acceptError() signal + + // SSL/Authentication Signals + void OriginAuthRequired(QWebSocketCorsAuthenticator*); //originAuthenticationRequired() signal + void PeerVerifyError(const QSslError&); //peerVerifyError() signal + void SslErrors(const QList&); //sslErrors() signal + + void SocketClosed(QString ID); + + //File watcher signals + void WatcherUpdate(QString); + +signals: + void DispatchStatusUpdate(QString); + +}; + +#endif \ No newline at end of file diff --git a/src/server/WebSocket.cpp b/src/server/WebSocket.cpp new file mode 100644 index 0000000..8f526f2 --- /dev/null +++ b/src/server/WebSocket.cpp @@ -0,0 +1,363 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include "WebSocket.h" + +#define DEBUG 0 +#define IDLETIMEOUTMINS 30 + +WebSocket::WebSocket(QWebSocket *sock, QString ID, AuthorizationManager *auth){ + SockID = ID; + SockAuthToken.clear(); //nothing set initially + SOCKET = sock; + SendAppCafeEvents = false; + AUTHSYSTEM = auth; + idletimer = new QTimer(this); + idletimer->setInterval(IDLETIMEOUTMINS*60000); //connection timout for idle sockets + idletimer->setSingleShot(true); + connect(idletimer, SIGNAL(timeout()), this, SLOT(checkIdle()) ); + connect(SOCKET, SIGNAL(textMessageReceived(const QString&)), this, SLOT(EvaluateMessage(const QString&)) ); + connect(SOCKET, SIGNAL(binaryMessageReceived(const QByteArray&)), this, SLOT(EvaluateMessage(const QByteArray&)) ); + connect(SOCKET, SIGNAL(aboutToClose()), this, SLOT(SocketClosing()) ); + idletimer->start(); +} + +WebSocket::~WebSocket(){ + if(SOCKET!=0){ + SOCKET->close(); + } + delete SOCKET; +} + + +QString WebSocket::ID(){ + return SockID; +} + +void WebSocket::setLastDispatch(QString msg){ + //used on initialization only + lastDispatchEvent = msg; +} + +//======================= +// PRIVATE +//======================= +void WebSocket::EvaluateREST(QString msg){ + //Parse the message into it's elements and proceed to the main data evaluation + RestInputStruct IN(msg); + //NOTE: All the REST functionality is disabled for the moment, until we decide to turn it on again at a later time (just need websockets right now - not full REST) + + if(DEBUG){ + qDebug() << "New REST Message:"; + qDebug() << " VERB:" << IN.VERB << "URI:" << IN.URI; + qDebug() << " HEADERS:" << IN.Header; + qDebug() << " BODY:" << IN.Body; + } + //Now check for the REST-specific verbs/actions + if(IN.VERB == "OPTIONS" || IN.VERB == "HEAD"){ + RestOutputStruct out; + out.CODE = RestOutputStruct::OK; + if(IN.VERB=="HEAD"){ + + }else{ //OPTIONS + out.Header << "Allow: HEAD, GET"; + out.Header << "Hosts: /syscache"; + } + out.Header << "Accept: text/json"; + out.Header << "Content-Type: text/json; charset=utf-8"; + SOCKET->sendTextMessage(out.assembleMessage()); + }else{ + EvaluateRequest(IN); + } +} + +void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ + RestOutputStruct out; + if(REQ.VERB != "GET"){ + //Non-supported request (at the moment) - return an error message + out.CODE = RestOutputStruct::BADREQUEST; + }else{ + //GET request + //Now check the body of the message and do what it needs + QJsonDocument doc = QJsonDocument::fromJson(REQ.Body.toUtf8()); + if(doc.isNull()){ qWarning() << "Empty JSON Message Body!!" << REQ.Body.toUtf8(); } + //Define the output structures + QJsonObject ret; //return message + + //Objects contain other key/value pairs - this is 99% of cases + if(doc.isObject()){ + //First check/set all the various required fields (both in and out) + bool good = doc.object().contains("namespace") \ + && doc.object().contains("name") \ + && doc.object().contains("id") \ + && doc.object().contains("args"); + //Can add some fallbacks for missing fields here - but not implemented yet + + //parse the message and do something + if(good && (JsonValueToString(doc.object().value("namespace"))=="rpc") ){ + //Now fetch the outputs from the appropriate subsection + //Note: Each subsection needs to set the "name", "namespace", and "args" output objects + QString name = JsonValueToString(doc.object().value("name")).toLower(); + QJsonValue args = doc.object().value("args"); + if(name.startsWith("auth")){ + //Now perform authentication based on type of auth given + //Note: This sets/changes the current SockAuthToken + AUTHSYSTEM->clearAuth(SockAuthToken); //new auth requested - clear any old token + if(DEBUG){ qDebug() << "Authenticate Peer:" << SOCKET->peerAddress().toString(); } + bool localhost = (SOCKET->peerAddress() == QHostAddress::LocalHost) || (SOCKET->peerAddress() == QHostAddress::LocalHostIPv6); + //Now do the auth + if(name=="auth" && args.isObject() ){ + //username/password authentication + QString user, pass; + if(args.toObject().contains("username")){ user = JsonValueToString(args.toObject().value("username")); } + if(args.toObject().contains("password")){ pass = JsonValueToString(args.toObject().value("password")); } + SockAuthToken = AUTHSYSTEM->LoginUP(localhost, user, pass); + }else if(name == "auth_token" && args.isObject()){ + SockAuthToken = JsonValueToString(args.toObject().value("token")); + }else if(name == "auth_clear"){ + return; //don't send a return message after clearing an auth (already done) + } + + //Now check the auth and respond appropriately + if(AUTHSYSTEM->checkAuth(SockAuthToken)){ + //Good Authentication - return the new token + ret.insert("namespace", QJsonValue("rpc")); + ret.insert("name", QJsonValue("response")); + ret.insert("id", doc.object().value("id")); //use the same ID for the return message + QJsonArray array; + array.append(SockAuthToken); + array.append(AUTHSYSTEM->checkAuthTimeoutSecs(SockAuthToken)); + ret.insert("args", array); + }else{ + SockAuthToken.clear(); //invalid token + //Bad Authentication - return error + SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + } + + }else if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token + //Now provide access to the various subsystems + //Pre-set any output fields + QJsonObject outargs; + ret.insert("namespace", QJsonValue("rpc")); + ret.insert("name", QJsonValue("response")); + ret.insert("id", doc.object().value("id")); //use the same ID for the return message + EvaluateBackendRequest(name, doc.object().value("args"), &outargs); + ret.insert("args",outargs); + }else{ + //Bad/No authentication + SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + } + + }else if(good && (JsonValueToString(doc.object().value("namespace"))=="events") ){ + if( AUTHSYSTEM->checkAuth(SockAuthToken) ){ //validate current Authentication token + //Pre-set any output fields + QJsonObject outargs; + ret.insert("namespace", QJsonValue("events")); + ret.insert("name", QJsonValue("response")); + ret.insert("id", doc.object().value("id")); //use the same ID for the return message + //Assemble the list of input events + QStringList evlist; + if(doc.object().value("args").isObject()){ evlist << JsonValueToString(doc.object().value("args")); } + else if(doc.object().value("args").isArray()){ evlist = JsonArrayToStringList(doc.object().value("args").toArray()); } + //Now subscribe/unsubscribe to these events + if(JsonValueToString(doc.object().value("name"))=="subscribe"){ + if(evlist.contains("dispatcher")){ + SendAppCafeEvents = true; + outargs.insert("subscribe",QJsonValue("dispatcher")); + QTimer::singleShot(100, this, SLOT(AppCafeStatusUpdate()) ); + } + }else if(JsonValueToString(doc.object().value("name"))=="unsubscribe"){ + if(evlist.contains("dispatcher")){ + SendAppCafeEvents = false; + outargs.insert("unsubscribe",QJsonValue("dispatcher")); + } + }else{ + outargs.insert("unknown",QJsonValue("unknown")); + } + ret.insert("args",outargs); + }else{ + //Bad/No authentication + SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + } + }else{ + //Error in inputs - assemble the return error message + QString id = "error"; + if(doc.object().contains("id")){ id = JsonValueToString(doc.object().value("id")); } //use the same ID + SetOutputError(&ret, id, 400, "Bad Request"); + } + }else{ + //Unknown type of JSON input - nothing to do + } + //Assemble the outputs for this "GET" request + out.CODE = RestOutputStruct::OK; + //Assemble the output JSON document/text + QJsonDocument retdoc; + retdoc.setObject(ret); + out.Body = retdoc.toJson(); + out.Header << "Content-Type: text/json; charset=utf-8"; + } + //Return any information + SOCKET->sendTextMessage(out.assembleMessage()); +} + +// === SYSCACHE REQUEST INTERACTION === +/*void WebSocket::EvaluateBackendRequest(QString name, const QJsonValue args, QJsonObject *out){ + QJsonObject obj; //output object + if(args.isObject()){ + //For the moment: all arguments are full syscache DB calls - no special ones + QStringList reqs = args.toObject().keys(); + if(!reqs.isEmpty()){ + if(DEBUG){ qDebug() << "Parsing Inputs:" << reqs; } + for(int r=0; rinsert(req, QJsonValue(values.join("")) ); } + else{ + //This is an array of outputs + QJsonArray arr; + for(int i=0; iinsert(req,arr); + } + } + } //end of special "request" objects + }else if(args.isArray()){ + QStringList inputs = JsonArrayToStringList(args.toArray()); + if(DEBUG){ qDebug() << "Parsing Array inputs:" << inputs; } + QStringList values; + if(name.toLower()=="syscache"){values = SysCacheClient::parseInputs( inputs ); } + else if(name.toLower()=="dispatcher"){values = DispatcherClient::parseInputs( inputs , AUTHSYSTEM); } + if(DEBUG){ qDebug() << " - Returns:" << values; } + for(int i=0; iinsert(inputs[i],arr); + }else{ + out->insert(inputs[i],values[i]); + } + } + } //end array of inputs + +}*/ + +// === GENERAL PURPOSE UTILITY FUNCTIONS === +QString WebSocket::JsonValueToString(QJsonValue val){ + //Note: Do not use this on arrays - only use this on single-value values + QString out; + switch(val.type()){ + case QJsonValue::Bool: + out = (val.toBool() ? "true": "false"); break; + case QJsonValue::Double: + out = QString::number(val.toDouble()); break; + case QJsonValue::String: + out = val.toString(); break; + case QJsonValue::Array: + out = "\""+JsonArrayToStringList(val.toArray()).join("\" \"")+"\""; + default: + out.clear(); + } + return out; +} + +QStringList WebSocket::JsonArrayToStringList(QJsonArray array){ + //Note: This assumes that the array is only values, not additional objects + QStringList out; + qDebug() << "Array to List:" << array.count(); + for(int i=0; iinsert("namespace", QJsonValue("rpc")); + ret->insert("name", QJsonValue("error")); + ret->insert("id",QJsonValue(id)); + QJsonObject obj; + obj.insert("code", err); + obj.insert("message", QJsonValue(msg)); + ret->insert("args",obj); +} + +// ===================== +// PRIVATE SLOTS +// ===================== +void WebSocket::checkIdle(){ + //This function is called automatically every few seconds that a client is connected + if(SOCKET !=0){ + qDebug() << " - Client Timeout: Closing connection..."; + SOCKET->close(); //timeout - close the connection to make way for others + } +} + +void WebSocket::SocketClosing(){ + qDebug() << "Socket Closing..."; + if(idletimer->isActive()){ + //This means the client deliberately closed the connection - not the idle timer + idletimer->stop(); + } + //Stop any current requests + + //Reset the pointer + SOCKET = 0; + emit SocketClosed(SockID); +} + +void WebSocket::EvaluateMessage(const QByteArray &msg){ + qDebug() << "New Binary Message:"; + if(idletimer->isActive()){ idletimer->stop(); } + EvaluateREST( QString(msg) ); + idletimer->start(); + qDebug() << "Done with Message"; +} + +void WebSocket::EvaluateMessage(const QString &msg){ + qDebug() << "New Text Message:"; + if(idletimer->isActive()){ idletimer->stop(); } + EvaluateREST(msg); + idletimer->start(); + qDebug() << "Done with Message"; +} + +// ====================== +// PUBLIC SLOTS +// ====================== +void WebSocket::AppCafeStatusUpdate(QString msg){ + if(!msg.isEmpty()){ lastDispatchEvent = msg; } + else{ msg = lastDispatchEvent; } + //qDebug() << "Socket Status Update:" << msg; + if(!SendAppCafeEvents){ return; } //don't report events on this socket + RestOutputStruct out; + //Define the output structures + QJsonObject ret; //return message + //Pre-set any output fields + QJsonObject outargs; + ret.insert("namespace", QJsonValue("events")); + ret.insert("name", QJsonValue("event")); + ret.insert("id", QJsonValue("")); + outargs.insert("name", "dispatcher"); + outargs.insert("args",QJsonValue(msg)); + ret.insert("args",outargs); + out.CODE = RestOutputStruct::OK; + //Assemble the output JSON document/text + QJsonDocument retdoc; + retdoc.setObject(ret); + out.Body = retdoc.toJson(); + out.Header << "Content-Type: text/json; charset=utf-8"; + SOCKET->sendTextMessage(out.assembleMessage()); +} diff --git a/src/server/WebSocket.h b/src/server/WebSocket.h new file mode 100644 index 0000000..5f37f83 --- /dev/null +++ b/src/server/WebSocket.h @@ -0,0 +1,66 @@ +// =============================== +// PC-BSD REST/JSON API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#ifndef _PCBSD_REST_WEB_SOCKET_H +#define _PCBSD_REST_WEB_SOCKET_H + +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "RestStructs.h" +#include "AuthorizationManager.h" + +class WebSocket : public QObject{ + Q_OBJECT +public: + WebSocket(QWebSocket*, QString ID, AuthorizationManager *auth); + ~WebSocket(); + + QString ID(); + void setLastDispatch(QString); //used on initialization only + +private: + QTimer *idletimer; + QWebSocket *SOCKET; + QString SockID, SockAuthToken, lastDispatchEvent; + AuthorizationManager *AUTHSYSTEM; + bool SendAppCafeEvents; + + //Main connection comminucations procedure + void EvaluateREST(QString); //Text -> Rest/JSON struct + void EvaluateRequest(const RestInputStruct&); // Parse Rest/JSON (does auth/events) + + //Simplification functions + QString JsonValueToString(QJsonValue); + QStringList JsonArrayToStringList(QJsonArray); + void SetOutputError(QJsonObject *ret, QString id, int err, QString msg); + + //Backend request/reply functions (contained in WebBackend.cpp) + void EvaluateBackendRequest(QString name, const QJsonValue in_args, QJsonObject *out); + +private slots: + void checkIdle(); //see if the currently-connected client is idle + void SocketClosing(); + + //Currently connected socket signal/slot connections + void EvaluateMessage(const QByteArray&); + void EvaluateMessage(const QString&); + +public slots: + void AppCafeStatusUpdate(QString msg = ""); + +signals: + void SocketClosed(QString); //ID + +}; + +#endif diff --git a/src/server/dispatcher-client.cpp b/src/server/dispatcher-client.cpp new file mode 100644 index 0000000..008ee24 --- /dev/null +++ b/src/server/dispatcher-client.cpp @@ -0,0 +1,84 @@ +#include "dispatcher-client.h" +#include +#include + +#define DISPATCH QString("/usr/local/share/appcafe/dispatcher") +#define DISPATCHIDFILE QString("/var/tmp/appcafe/dispatch-id") +#define DISPATCHENVVAR QString("PHP_DISID") + +DispatcherClient::DispatcherClient(AuthorizationManager *auth, QObject *parent) : QProcess(parent){ + this->setProcessChannelMode(QProcess::MergedChannels); + AUTH = auth; +} + +DispatcherClient::~DispatcherClient(){ +} + +bool DispatcherClient::setupProcAuth(){ + //First check that the dispatcher binary actually exists + if(!QFile::exists(DISPATCH) || AUTH==0){ qWarning() << "AppCafe Dispatcher binary not found:"; return false; } + //Now check the current authorization key + QString key = ReadKey(); + if(!AUTH->checkAuth(key) ){ + //Key now invalid - generate a new one (this ensures that the secure key rotates on a regular basis) + key = AUTH->LoginService(true, "dispatcher"); + //Save the auth key to the file and lock it down + if(!WriteKey(key)){ + qWarning() << "Could not save dispatcher authorization key: **No dispatcher availability**. "; + AUTH->clearAuth(key); + return false; + } + } + //Now put that key into the process environment for the dispatcher to see/verify + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("LANG", "C"); + env.insert("LC_ALL", "C"); + env.insert(DISPATCHENVVAR, key); + this->setProcessEnvironment(env); + return true; +} + +QString DispatcherClient::GetProcOutput(QString args){ + this->start(DISPATCH+" "+args); + if(!this->waitForStarted(5000)){ return ""; } //process never started - max wait of 5 seconds + while(!this->waitForFinished(1000)){ + if(this->state() != QProcess::Running){ break; } //somehow missed the finished signal + QCoreApplication::processEvents(); + } + return QString(this->readAllStandardOutput()); +} + +QStringList DispatcherClient::parseInputs(QStringList inputs, AuthorizationManager *auth){ + DispatcherClient client(auth); + if(!client.setupProcAuth()){ return QStringList(); } //unauthorized + QStringList outputs; + for(int i=0; i +#include +#include +#include + +#include "AuthorizationManager.h" + +class DispatcherClient : public QProcess{ + Q_OBJECT +public: + DispatcherClient(AuthorizationManager *auth, QObject *parent=0); + ~DispatcherClient(); + + bool setupProcAuth(); + QString GetProcOutput(QString args); + + //Static function to run a request and wait for it to finish before returning + static QStringList parseInputs(QStringList inputs, AuthorizationManager *auth); + +private: + AuthorizationManager *AUTH; + + QString ReadKey(); + bool WriteKey(QString key); +}; + +#endif diff --git a/src/server/main.cpp b/src/server/main.cpp new file mode 100644 index 0000000..3d15460 --- /dev/null +++ b/src/server/main.cpp @@ -0,0 +1,81 @@ +// =============================== +// PC-BSD REST API Server +// Available under the 3-clause BSD License +// Written by: Ken Moore July 2015 +// ================================= +#include +#include +#include +#include +#include +#include +#include + +#include "WebServer.h" + +#ifndef PREFIX +#define PREFIX QString("/usr/local/") +#endif + +#define DEBUG 1 + +QFile logfile("/var/log/syscache-webclient.log"); +void MessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg){ + QString txt; + switch(type){ + case QtDebugMsg: + txt = msg; + break; + case QtWarningMsg: + txt = QString("WARNING: %1").arg(msg); + txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function); + break; + case QtCriticalMsg: + txt = QString("CRITICAL: %1").arg(msg); + txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function); + break; + case QtFatalMsg: + txt = QString("FATAL: %1").arg(msg); + txt += "\n Context: "+QString(context.file)+" Line: "+QString(context.line)+" Function: "+QString(context.function); + break; + } + + QTextStream out(&logfile); + out << txt; + if(!txt.endsWith("\n")){ out << "\n"; } +} + +int main( int argc, char ** argv ) +{ + QCoreApplication a(argc, argv); + //Check whether running as root + if( getuid() != 0){ + qDebug() << "syscache-webclient must be started as root!"; + return 1; + } + //Setup the log file + if(DEBUG){ + qDebug() << "Log File:" << logfile.fileName(); + if(QFile::exists(logfile.fileName()+".old")){ QFile::remove(logfile.fileName()+".old"); } + if(logfile.exists()){ QFile::rename(logfile.fileName(), logfile.fileName()+".old"); } + //Make sure the parent directory exists + if(!QFile::exists("/var/log")){ + QDir dir; + dir.mkpath("/var/log"); + } + logfile.open(QIODevice::WriteOnly | QIODevice::Append); + qInstallMessageHandler(MessageOutput); + } + + //Create and start the daemon + qDebug() << "Starting the PC-BSD syscache websocket client interface...."; + WebServer *w = new WebServer(); + if( w->startServer() ){ + //Now start the event loop + int ret = a.exec(); + logfile.close(); + return ret; + }else{ + return 1; + } +} diff --git a/src/server/server.pro b/src/server/server.pro new file mode 100644 index 0000000..fb97d2a --- /dev/null +++ b/src/server/server.pro @@ -0,0 +1,33 @@ +TEMPLATE = app +LANGUAGE = C++ + +CONFIG += qt warn_on release +QT = core network websockets + +HEADERS += WebServer.h \ + WebSocket.h \ + syscache-client.h \ + dispatcher-client.h \ + RestStructs.h \ + AuthorizationManager.h + +SOURCES += main.cpp \ + WebServer.cpp \ + WebSocket.cpp \ + WebBackend.cpp \ + syscache-client.cpp \ + dispatcher-client.cpp \ + AuthorizationManager.cpp + + +TARGET=syscache-webclient +target.path=/usr/local/bin + + +INSTALLS += target + + +QMAKE_LIBDIR = /usr/local/lib/qt5 /usr/local/lib + +INCLUDEPATH += /usr/local/include +LIBS += -L../library -L/usr/local/lib -lpam -lutil -lsysadm \ No newline at end of file diff --git a/src/server/syscache-client.cpp b/src/server/syscache-client.cpp new file mode 100644 index 0000000..e5b1192 --- /dev/null +++ b/src/server/syscache-client.cpp @@ -0,0 +1,85 @@ +#include +#include "syscache-client.h" +#include + +#define LINEBREAK QString("") + +#define DEBUG 1 +SysCacheClient::SysCacheClient(QObject *parent) : QLocalSocket(parent){ + connect(this, SIGNAL(connected()), this, SLOT(startRequest())); + connect(this, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(connectionError())); + connect(this, SIGNAL(readyRead()), this, SLOT(requestFinished()) ); +} + +SysCacheClient::~SysCacheClient(){ +} + +QStringList SysCacheClient::parseInputs(QStringList inputs){ + SysCacheClient client; + client.userRequest = inputs; + client.servRequest = inputs; + if(DEBUG){ qDebug() << "Syscache Request:" << inputs; } + //Convert the user request into server request formatting + + + //Now start the connection to the server + client.connectToServer("/var/run/syscache.pipe", QIODevice::ReadWrite | QIODevice::Text); + QCoreApplication::processEvents(); + usleep(100); + //Wait for the socket to connect to the server + while(client.state() != QLocalSocket::ConnectedState){ + usleep(100); //this connection should happen very fast + QCoreApplication::processEvents(); + } + //qDebug() << "Syscache connected"; + //Now wait for the server to process the request and send a reply + while( client.state() != QLocalSocket::UnconnectedState && client.isValid() && client.ans.isEmpty()){ + usleep(400); + QCoreApplication::processEvents(); + } + //qDebug() << " - Syscache disconnected:" << client.ans; + return client.ans; +} + + +void SysCacheClient::startRequest(){ + ans.clear(); + QTextStream out(this); + servRequest.prepend("[NONCLI]"); //put the special non-CLI client flag in place + out << servRequest.join("\n[/]\n"); + out << "\n[FINISHED]"; + +} + +void SysCacheClient::requestFinished(){ + static bool running = false; + if(running){ return; } //already reading stream + running = true; + if(DEBUG){ qDebug() << "Client Request Finished"; } + QTextStream in(this); + QString line; + while(!line.endsWith("[FINISHED]")){ + line.append(in.readLine()); + QCoreApplication::processEvents(); + } + line.remove("[FINISHED]"); + if(DEBUG){ qDebug() << "Reply:" << line; } + QStringList output = line.split("[INFOSTART]"); + output.removeAll("[/]"); + output.removeAll(""); + if(DEBUG){ qDebug() << " - In List:" << output; } + ans = output; //save it for later + running = false; + //qDebug() << " - Syscache connection closing" << ans; + this->disconnectFromServer(); +} + +void SysCacheClient::connectionError(){ + qDebug() << "Client Connection Error:"; + if(this->error()==QLocalSocket::PeerClosedError){ + //requestFinished(); + }else{ + qDebug() << "[ERROR]" << this->errorString(); + qDebug() << " - Is the syscache daemon running?"; + } +} diff --git a/src/server/syscache-client.h b/src/server/syscache-client.h new file mode 100644 index 0000000..68382bb --- /dev/null +++ b/src/server/syscache-client.h @@ -0,0 +1,33 @@ +#ifndef _WEB_SERVER_SYSCACHE_CLIENT_MAIN_H +#define _WEB_SERVER_SYSCACHE_CLIENT_MAIN_H + +#include +#include +#include +#include +#include +#include +#include + +class SysCacheClient : public QLocalSocket{ + Q_OBJECT +public: + SysCacheClient(QObject *parent=0); + ~SysCacheClient(); + + //Static function to run a request and wait for it to finish before returning + static QStringList parseInputs(QStringList inputs); + + //input/output variables + QStringList userRequest, servRequest, ans; + + +private slots: + //Server/Client connections + void startRequest(); + void requestFinished(); + void connectionError(); + +}; + +#endif diff --git a/src/sysadm.pro b/src/sysadm.pro index ff8a62c..dcf3d96 100644 --- a/src/sysadm.pro +++ b/src/sysadm.pro @@ -1,8 +1,9 @@ TEMPLATE = subdirs CONFIG += recursive -SUBDIRS+= library binary +SUBDIRS+= library binary server #Make sure to list the library as a requirement for the others (for parallellized builds) binary.depends = library +server.depends = library