Add a copy of the syscache-webclient server into the sysadm/src/server.

Also setup the server to make it easier to extend for API/library support in the backend. All backend functionality can now be added to the new "WebBackend.cpp" file (and WebSocket.h file for headers).
This commit is contained in:
Ken Moore
2015-12-15 13:06:42 -05:00
parent 374528f38f
commit f5dab0048c
17 changed files with 1808 additions and 1 deletions

View File

@@ -0,0 +1,203 @@
// ===============================
// PC-BSD REST/JSON API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#include "AuthorizationManager.h"
#include <QDebug>
#include <QProcess>
#include <QCoreApplication>
// Stuff for PAM to work
#include <sys/types.h>
#include <security/pam_appl.h>
#include <security/openpam.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pwd.h>
#include <login_cap.h>
//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<TOKENLENGTH; i++){
tok.append( AUTHCHARS.at( qrand() % AUTHCHARS.length() ) );
}
if(HASH.contains(tok)){
//Just in case the randomizer came up with something identical - re-run it
tok = generateNewToken();
}else{
//unique token created - add it to the hash with the current time (+timeout)
HASH.insert(tok, QDateTime::currentDateTime().addSecs(TIMEOUTSECS) );
}
return tok;
}
QStringList AuthorizationManager::getUserGroups(QString user){
QProcess proc;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert("LANG", "C");
env.insert("LC_All", "C");
proc.setProcessEnvironment(env);
proc.setProcessChannelMode(QProcess::MergedChannels);
proc.start("id", QStringList() << "-nG" << user);
if(!proc.waitForStarted(30000)){ return QStringList(); } //process never started - max wait of 30 seconds
while(!proc.waitForFinished(500)){
if(proc.state() != QProcess::Running){ break; } //somehow missed the finished signal
QCoreApplication::processEvents();
}
QStringList out = QString(proc.readAllStandardOutput()).split(" ");
return out;
}
/*
========== PAM FUNCTIONS ==========
*/
static struct pam_conv pamc = { openpam_nullconv, NULL };
pam_handle_t *pamh;
bool AuthorizationManager::pam_checkPW(QString user, QString pass){
//Convert the inputs to C character arrays for use in PAM
QByteArray tmp = user.toUtf8();
char* cUser = tmp.data();
QByteArray tmp2 = pass.toUtf8();
char* cPassword = tmp2.data();
//initialize variables
bool result = false;
int ret;
//Initialize PAM
ret = pam_start( user=="root" ? "system": "login", cUser, &pamc, &pamh);
if( ret == PAM_SUCCESS ){
//Place the user-supplied password into the structure
ret = pam_set_item(pamh, PAM_AUTHTOK, cPassword);
//Set the TTY
//ret = pam_set_item(pamh, PAM_TTY, "pcdm-terminal");
//Authenticate with PAM
ret = pam_authenticate(pamh,0);
if( ret == PAM_SUCCESS ){
//Check for valid, unexpired account and verify access restrictions
ret = pam_acct_mgmt(pamh,0);
if( ret == PAM_SUCCESS ){ result = true; }
}else{
pam_logFailure(ret);
}
}
//return verification result
return result;
}
void AuthorizationManager::pam_logFailure(int ret){
//Interpret a PAM error message and log it
qWarning() << "PAM Error: " << ret;
switch( ret ){
case PAM_ABORT:
qWarning() << " - PAM abort error";
break;
case PAM_AUTHINFO_UNAVAIL:
qWarning() << " - Authentication info unavailable";
break;
case PAM_AUTH_ERR:
qWarning() << " - Authentication error";
break;
case PAM_BUF_ERR:
qWarning() << " - Buffer error";
break;
case PAM_CONV_ERR:
qWarning() << " - Conversion error";
break;
case PAM_CRED_INSUFFICIENT:
qWarning() << " - Credentials insufficient";
break;
case PAM_MAXTRIES:
qWarning() << " - Maximum number of tries exceeded";
break;
case PAM_PERM_DENIED:
qWarning() << " - Permission denied";
break;
case PAM_SERVICE_ERR:
qWarning() << " - Service error";
break;
case PAM_SYMBOL_ERR:
qWarning() << " - Symbol error";
break;
case PAM_SYSTEM_ERR:
qWarning() << " - System error";
break;
case PAM_USER_UNKNOWN:
qWarning() << " - Unknown user";
break;
default:
qWarning() << " - Unrecognized authentication error";
}
}

View File

@@ -0,0 +1,37 @@
// ===============================
// PC-BSD REST/JSON API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#ifndef _PCBSD_REST_AUTHORIZATION_MANAGER_H
#define _PCBSD_REST_AUTHORIZATION_MANAGER_H
#include <QHash>
#include <QString>
#include <QDateTime>
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<QString, QDateTime> HASH;
QString generateNewToken();
QStringList getUserGroups(QString user);
//PAM login/check files
bool pam_checkPW(QString user, QString pass);
void pam_logFailure(int ret);
};
#endif

105
src/server/RestStructs.h Normal file
View File

@@ -0,0 +1,105 @@
// ===============================
// PC-BSD REST/JSON API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#ifndef _PCBSD_REST_SERVER_REST_STRUCTS_H
#define _PCBSD_REST_SERVER_REST_STRUCTS_H
#include <QString>
#include <QStringList>
#include <QDateTime>
#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

View File

@@ -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 "<jail>" 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" : "<message"
}
}
===================================
- Dispatcher Usage
===================================
JSON flags to for dispatcher interaction
"namespace": "rpc"
"name": "dispatcher"
Usage (possible "args"):
--------------------------------
iocage {cmd} [args]
queue {pkg|pbi} {origin} {install/delete/info} {__system__|<jailname>}
pkgupdate {__system__|<jailname>}
service {start|stop|restart} {servicetag} {servicerc} {__system__|<jid>}
getcfg {pbicdir} {__system__|<jid>} {key}
setcfg {pbicdir} {__system__|<jid>} {key} {value}
donecfg {pbicdir} {__system__|<jid>}
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
<jail> app-summary <pkg origin>: Summary of info for an application
cage-summary <origin>: 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...<br>Your system is up to date!"
},
"id": "someUniqueID",
"name": "response",
"namespace": "rpc"
}
======================
- PBI database queries/examples
======================
List Queries: "pbi list <info>" where <info> can be: "[all/server/graphical/text]apps", "[all/server/graphical/text]cats", or "cages"
App Queries: "pbi app <pkg origin> <info>" where <info> 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 <origin> <info>" where <info> can be: "icon", "name", "description", "arch" fbsdver", "git", "gitbranch", "screenshots", "tags", "website"
Category Queries: "pbi cat <pkg category> <info>
-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 <origin> <info>"
Possible <info>: "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 <jail> <info>" where <info> can be: "remotelist", "installedlist", "hasupdates" (true/false returned), or "updatemessage".
Individual pkg queries: "pkg <jail> <local/remote> <pkg origin> <info>"
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)
<info> may be: "origin", "name", "version", "maintainer", "comment", "description", "website", "size", "arch", "message", "dependencies", "rdependencies", "categories", "options", "license"
For "local" pkgs, there are some additional <info> 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: "<pkg/pbi> search <search term> [<pkg jail>/<pbi filter>] [result minimum]
The search always returns an array of <pkg origin>, 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 "<pkg jail>" option may only be used for pkg searches, and corresponds to normal <jail> syntax ("#system" or jail ID). If it is not supplied, it assumes a search for the local system (#system).
The "<pbi filter>" 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"
}

68
src/server/WebBackend.cpp Normal file
View File

@@ -0,0 +1,68 @@
// ===============================
// PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> DEC 2015
// =================================
#include <WebSocket.h>
//sysadm library interface classes
#include <sysadm-general.h>
#include <sysadm-network.h>
#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; r<reqs.length(); r++){
QString req = JsonValueToString(args.toObject().value(reqs[r]));
if(DEBUG){ qDebug() << " ["+reqs[r]+"]="+req; }
QStringList values;
if(name.toLower()=="syscache"){values = SysCacheClient::parseInputs( QStringList() << req ); }
else if(name.toLower()=="dispatcher"){values = DispatcherClient::parseInputs( QStringList() << req, AUTHSYSTEM); }
values.removeAll("");
//Quick check if a list of outputs was returned
if(values.length()==1 && name.toLower()=="syscache"){
values = values[0].split(SCLISTDELIM); //split up the return list (if necessary)
values.removeAll("");
}
if(DEBUG){ qDebug() << " - Returns:" << values; }
if(values.length()<2){ out->insert(req, QJsonValue(values.join("")) ); }
else{
//This is an array of outputs
QJsonArray arr;
for(int i=0; i<values.length(); i++){ arr.append(values[i]); }
out->insert(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; i<values.length(); i++){
if(name.toLower()=="syscache" && values[i].contains(SCLISTDELIM)){
//This is an array of values from syscache
QStringList vals = values[i].split(SCLISTDELIM);
vals.removeAll("");
QJsonArray arr;
for(int j=0; j<vals.length(); j++){ arr.append(vals[j]); }
out->insert(inputs[i],arr);
}else{
out->insert(inputs[i],values[i]);
}
}
} //end array of inputs
}

17
src/server/WebBackend.h Normal file
View File

@@ -0,0 +1,17 @@
// ===============================
// PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> DEC 2015
// =================================
#ifndef _SYSADM_WEBSERVER_BACKEND_CLASS_H
#define _SYSADM_WEBSERVER_BACKEND_CLASS_H
//sysadm library interface classes
#include <sysadm-general.h>
#include <sysadm-network.h>
#include "syscache-client.h"
#endif

175
src/server/WebServer.cpp Normal file
View File

@@ -0,0 +1,175 @@
// ===============================
// PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#include "WebServer.h"
#include <QCoreApplication>
#include <QUrl>
#include <QFile>
#include <QTextStream>
#include <QProcess>
#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<QSslError>&)), this, SLOT(SslErrors(const QList<QSslError>&)) );
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; i<OpenSockets.length(); i++){
if(OpenSockets[i]->ID().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<QSslError> &list){
qWarning() << "SSL Errors:";
for(int i=0; i<list.length(); i++){
qWarning() << " - " << list[i].errorString();
}
}
void WebServer::SocketClosed(QString ID){
for(int i=0; i<OpenSockets.length(); i++){
if(OpenSockets[i]->ID()==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
}
}

67
src/server/WebServer.h Normal file
View File

@@ -0,0 +1,67 @@
// ===============================
// PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#ifndef _PCBSD_REST_WEB_SERVER_H
#define _PCBSD_REST_WEB_SERVER_H
#include <QWebSocketServer>
#include <QWebSocket>
#include <QWebSocketCorsAuthenticator>
#include <QFileSystemWatcher>
#include <QSslError>
#include <QList>
#include <QObject>
#include <QTimer>
#include <QDebug>
#include <QtDebug> //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<WebSocket*> 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<QSslError>&); //sslErrors() signal
void SocketClosed(QString ID);
//File watcher signals
void WatcherUpdate(QString);
signals:
void DispatchStatusUpdate(QString);
};
#endif

363
src/server/WebSocket.cpp Normal file
View File

@@ -0,0 +1,363 @@
// ===============================
// PC-BSD REST/JSON API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> 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; r<reqs.length(); r++){
QString req = JsonValueToString(args.toObject().value(reqs[r]));
if(DEBUG){ qDebug() << " ["+reqs[r]+"]="+req; }
QStringList values;
if(name.toLower()=="syscache"){values = SysCacheClient::parseInputs( QStringList() << req ); }
else if(name.toLower()=="dispatcher"){values = DispatcherClient::parseInputs( QStringList() << req, AUTHSYSTEM); }
values.removeAll("");
//Quick check if a list of outputs was returned
if(values.length()==1 && name.toLower()=="syscache"){
values = values[0].split(SCLISTDELIM); //split up the return list (if necessary)
values.removeAll("");
}
if(DEBUG){ qDebug() << " - Returns:" << values; }
if(values.length()<2){ out->insert(req, QJsonValue(values.join("")) ); }
else{
//This is an array of outputs
QJsonArray arr;
for(int i=0; i<values.length(); i++){ arr.append(values[i]); }
out->insert(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; i<values.length(); i++){
if(name.toLower()=="syscache" && values[i].contains(SCLISTDELIM)){
//This is an array of values from syscache
QStringList vals = values[i].split(SCLISTDELIM);
vals.removeAll("");
QJsonArray arr;
for(int j=0; j<vals.length(); j++){ arr.append(vals[j]); }
out->insert(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; i<array.count(); i++){
out << JsonValueToString(array.at(i));
}
return out;
}
void WebSocket::SetOutputError(QJsonObject *ret, QString id, int err, QString msg){
ret->insert("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());
}

66
src/server/WebSocket.h Normal file
View File

@@ -0,0 +1,66 @@
// ===============================
// PC-BSD REST/JSON API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#ifndef _PCBSD_REST_WEB_SOCKET_H
#define _PCBSD_REST_WEB_SOCKET_H
#include <QWebSocket>
#include <QList>
#include <QObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QTimer>
#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

View File

@@ -0,0 +1,84 @@
#include "dispatcher-client.h"
#include <QFile>
#include <QCoreApplication>
#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<inputs.length(); i++){
outputs << client.GetProcOutput(inputs[i]);
}
return outputs;
}
QString DispatcherClient::ReadKey(){
QFile file(DISPATCHIDFILE);
if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){ return ""; }
QString key;
QTextStream in(&file);
key = in.readAll();
file.close();
return key;
}
bool DispatcherClient::WriteKey(QString key){
QFile file(DISPATCHIDFILE);
if(!file.open(QIODevice::WriteOnly | QIODevice::Text)){ return false; }
QTextStream out(&file);
out << key;
file.close();
//Now lock down the file (root only read/write)
if(!file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner)){
//Could not lock down the file - this is insecure and should not be used
file.remove();
return false;
}
return true;
}

View File

@@ -0,0 +1,30 @@
#ifndef _WEB_SERVER_DISPATCHER_CLIENT_MAIN_H
#define _WEB_SERVER_DISPATCHER_CLIENT_MAIN_H
#include <QString>
#include <QStringList>
#include <QProcess>
#include <QDebug>
#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

81
src/server/main.cpp Normal file
View File

@@ -0,0 +1,81 @@
// ===============================
// PC-BSD REST API Server
// Available under the 3-clause BSD License
// Written by: Ken Moore <ken@pcbsd.org> July 2015
// =================================
#include <QCoreApplication>
#include <QFile>
#include <QTextStream>
#include <QTimer>
#include <QDir>
#include <unistd.h>
#include <sys/types.h>
#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;
}
}

33
src/server/server.pro Normal file
View File

@@ -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

View File

@@ -0,0 +1,85 @@
#include <stdio.h>
#include "syscache-client.h"
#include <unistd.h>
#define LINEBREAK QString("<LINEBREAK>")
#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?";
}
}

View File

@@ -0,0 +1,33 @@
#ifndef _WEB_SERVER_SYSCACHE_CLIENT_MAIN_H
#define _WEB_SERVER_SYSCACHE_CLIENT_MAIN_H
#include <QString>
#include <QStringList>
#include <QObject>
#include <QTextStream>
#include <QLocalSocket>
#include <QCoreApplication>
#include <QDebug>
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

View File

@@ -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