mirror of
https://github.com/outbackdingo/sysadm.git
synced 2026-01-27 02:20:17 +00:00
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:
203
src/server/AuthorizationManager.cpp
Normal file
203
src/server/AuthorizationManager.cpp
Normal 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";
|
||||
}
|
||||
|
||||
}
|
||||
37
src/server/AuthorizationManager.h
Normal file
37
src/server/AuthorizationManager.h
Normal 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
105
src/server/RestStructs.h
Normal 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
|
||||
359
src/server/Syscache_websocket_examples.txt
Normal file
359
src/server/Syscache_websocket_examples.txt
Normal 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
68
src/server/WebBackend.cpp
Normal 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
17
src/server/WebBackend.h
Normal 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
175
src/server/WebServer.cpp
Normal 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
67
src/server/WebServer.h
Normal 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
363
src/server/WebSocket.cpp
Normal 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
66
src/server/WebSocket.h
Normal 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
|
||||
84
src/server/dispatcher-client.cpp
Normal file
84
src/server/dispatcher-client.cpp
Normal 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;
|
||||
}
|
||||
30
src/server/dispatcher-client.h
Normal file
30
src/server/dispatcher-client.h
Normal 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
81
src/server/main.cpp
Normal 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
33
src/server/server.pro
Normal 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
|
||||
85
src/server/syscache-client.cpp
Normal file
85
src/server/syscache-client.cpp
Normal 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?";
|
||||
}
|
||||
}
|
||||
33
src/server/syscache-client.h
Normal file
33
src/server/syscache-client.h
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user