From b0bfcb4c033035133307917562a2e17b2cc9a254 Mon Sep 17 00:00:00 2001 From: Ken Moore Date: Wed, 30 Dec 2015 16:05:08 -0500 Subject: [PATCH] Convert the REST/JSON input/output structures quite a bit so the backend usage is now agnostic as to the type of input used. It will then convert the output format to match what was input (REST -> REST, JSON -> JSON). --- src/server/RestStructs.h | 183 +++++++++++++++++++++++++++++---------- src/server/WebSocket.cpp | 132 +++++++++++++++------------- 2 files changed, 208 insertions(+), 107 deletions(-) diff --git a/src/server/RestStructs.h b/src/server/RestStructs.h index f371c58..7f25df9 100644 --- a/src/server/RestStructs.h +++ b/src/server/RestStructs.h @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #define CurHttpVersion QString("HTTP/1.1") @@ -22,12 +25,19 @@ */ class RestInputStruct{ public: + //REST Variables QString VERB, URI, HTTPVERSION; - QStringList Header; - QString Body; + //JSON input variables + QString name, namesp, id; + QJsonValue args; + //Raw Text + QStringList Header; //REST Headers + QString Body; //Everything else - RestInputStruct(QString message){ - HTTPVERSION = CurHttpVersion; + RestInputStruct(QString message = ""){ + HTTPVERSION = CurHttpVersion; //default value + if(message.isEmpty()){ return; } + //Pull out any REST headers if(!message.startsWith("{")){ Header = message.section("\n{",0,0).split("\n"); } @@ -37,11 +47,28 @@ public: 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; + } + //Now Parse out the Body into the JSON fields and/or arguments structure + Body = Body.simplified(); //remove any extra whitespace on the beginning/end + if(Body.startsWith("{") && Body.endsWith("}") ){ + QJsonDocument doc = QJsonDocument::fromJson(Body.toUtf8()); + if(!doc.isNull() && doc.isObject() ){ + //Valid JSON found + if(doc.object().contains("namespace") ){ namesp = doc.object().value("namespace").toString(); } + if(doc.object().contains("name") ){ name = doc.object().value("name").toString(); } + if(doc.object().contains("id") ){ namesp = doc.object().value("id").toString(); } + if(doc.object().contains("args") ){ args = doc.object().value("args"); } + else{ + //no args structure - treat the entire body as the arguments struct + args = doc.object(); + } + } + } + //Now do any REST -> JSON conversions if necessary + if(!URI.isEmpty()){ + //TO-DO + name = URI.section("/",-1); //last entry + namesp = URI.section("/",0,-2); //URI excluding name } } ~RestInputStruct(){} @@ -51,54 +78,114 @@ public: class RestOutputStruct{ public: enum ExitCode{OK, CREATED, ACCEPTED, NOCONTENT, RESETCONTENT, PARTIALCONTENT, PROCESSING, BADREQUEST, UNAUTHORIZED, FORBIDDEN, NOTFOUND }; - QString HTTPVERSION; - QStringList Header; + RestInputStruct in_struct; ExitCode CODE; - QString Body; + QStringList Header; //REST output header lines + QJsonValue out_args; 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; + if( !in_struct.VERB.isEmpty() ){ + //REST output syntax + QStringList headers; + QString firstline = in_struct.HTTPVERSION; + if(firstline.isEmpty()){ firstline = CurHttpVersion; }//default value + QString Body; + if(!out_args.isNull()){ + QJsonObject obj; obj.insert("args", out_args); + Body = QJsonDocument(obj).toJson(QJsonDocument::Compact); + } + 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); + 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"); + + }else{ + //JSON output (load all the input fields for the moment) + QString oname = "response"; //use this for valid responses (input ID found) + if(in_struct.id.isEmpty()){ oname = in_struct.name; } + QString onamesp = in_struct.namesp; + QString oid = in_struct.id; + if(CODE!=OK){ + //Format the output based on the type of error + QJsonObject out_err; + switch(CODE){ + case PROCESSING: + //oname = onamesp = "error"; + out_err.insert("code","102"); out_err.insert("message", "Processing"); break; + case CREATED: + //oname = onamesp = "error"; + out_err.insert("code","201"); out_err.insert("message", "Created"); break; + case ACCEPTED: + //oname = onamesp = "error"; + out_err.insert("code","202"); out_err.insert("message", "Accepted"); break; + case NOCONTENT: + oname = onamesp = "error"; + out_err.insert("code","204"); out_err.insert("message", "No Content"); break; + case RESETCONTENT: + //oname = onamesp = "error"; + out_err.insert("code","205"); out_err.insert("message", "Reset Content"); break; + case PARTIALCONTENT: + oname = onamesp = "error"; + out_err.insert("code","206"); out_err.insert("message", "Partial Content"); break; + case BADREQUEST: + oname = onamesp = "error"; + out_err.insert("code","400"); out_err.insert("message", "Bad Request"); break; + case UNAUTHORIZED: + oname = onamesp = "error"; + out_err.insert("code","401"); out_err.insert("message", "Unauthorized"); break; + case FORBIDDEN: + oname = onamesp = "error"; + out_err.insert("code","403"); out_err.insert("message", "Forbidden"); break; + case NOTFOUND: + oname = onamesp = "error"; + out_err.insert("code","404"); out_err.insert("message", "Not Found"); break; + default: + break; + } + out_args = out_err; + } + //Now assemple the JSON output + QJsonObject obj; + obj.insert("namespace",onamesp); + obj.insert("name",oname); + obj.insert("id",oid); + obj.insert("args", out_args); + //Convert the JSON to string form + QJsonDocument doc(obj); + return doc.toJson(QJsonDocument::Compact); } - 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; } }; diff --git a/src/server/WebSocket.cpp b/src/server/WebSocket.cpp index 125937b..bc61abd 100644 --- a/src/server/WebSocket.cpp +++ b/src/server/WebSocket.cpp @@ -79,6 +79,7 @@ void WebSocket::EvaluateREST(QString msg){ //Now check for the REST-specific verbs/actions if(IN.VERB == "OPTIONS" || IN.VERB == "HEAD"){ RestOutputStruct out; + out.in_struct = IN; out.CODE = RestOutputStruct::OK; if(IN.VERB=="HEAD"){ @@ -88,21 +89,26 @@ void WebSocket::EvaluateREST(QString msg){ } out.Header << "Accept: text/json"; out.Header << "Content-Type: text/json; charset=utf-8"; - SOCKET->sendTextMessage(out.assembleMessage()); - }else{ + if(SOCKET!=0){ SOCKET->sendTextMessage(out.assembleMessage()); } + else if(TSOCKET!=0){ TSOCKET->write(out.assembleMessage().toUtf8().data()); } + }else{ EvaluateRequest(IN); } } void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ RestOutputStruct out; - if(REQ.VERB != "GET"){ + out.in_struct = REQ; + if(!REQ.VERB.isEmpty() && REQ.VERB != "GET"){ //Non-supported request (at the moment) - return an error message out.CODE = RestOutputStruct::BADREQUEST; + }else if(out.in_struct.name.isEmpty() || out.in_struct.namesp.isEmpty() ){ + //Invalid JSON structure validity + //Note: id and args are optional at this stage - let the subsystems handle those inputs + 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()); + /*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 @@ -115,81 +121,85 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ && 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") ){ + //if(good && (JsonValueToString(doc.object().value("namespace"))=="rpc") ){ + if(out.in_struct.namesp.toLower() == "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")){ + //QString name = JsonValueToString(doc.object().value("name")).toLower(); + //QJsonValue args = doc.object().value("args"); + if(out.in_struct.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() ){ + if(out.in_struct.name=="auth" && out.in_struct.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")); } + if(out.in_struct.args.toObject().contains("username")){ user = JsonValueToString(out.in_struct.args.toObject().value("username")); } + if(out.in_struct.args.toObject().contains("password")){ pass = JsonValueToString(out.in_struct.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"){ + }else if(out.in_struct.name == "auth_token" && out.in_struct.args.isObject()){ + SockAuthToken = JsonValueToString(out.in_struct.args.toObject().value("token")); + }else if(out.in_struct.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 + //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); + out.out_args = array; }else{ SockAuthToken.clear(); //invalid token //Bad Authentication - return error - SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + out.CODE = RestOutputStruct::UNAUTHORIZED; + //SetOutputError(&ret, JsonValueToString(out.in_struct.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); + //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 + out.CODE = EvaluateBackendRequest(out.in_struct.name, out.in_struct.args, &outargs); + out.out_args = outargs; //ret.insert("args",outargs); }else{ + out.CODE = RestOutputStruct::UNAUTHORIZED; //Bad/No authentication - SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + //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 + + //}else if(good && (JsonValueToString(doc.object().value("namespace"))=="events") ){ + }else if(out.in_struct.namesp.toLower() == "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 + //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()); } + if(out.in_struct.args.isObject()){ evlist << JsonValueToString(out.in_struct.args); } + else if(out.in_struct.args.isArray()){ evlist = JsonArrayToStringList(out.in_struct.args.toArray()); } //Now subscribe/unsubscribe to these events - if(JsonValueToString(doc.object().value("name"))=="subscribe"){ + if(out.in_struct.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"){ + }else if(out.in_struct.name=="unsubscribe"){ if(evlist.contains("dispatcher")){ SendAppCafeEvents = false; outargs.insert("unsubscribe",QJsonValue("dispatcher")); @@ -197,26 +207,26 @@ void WebSocket::EvaluateRequest(const RestInputStruct &REQ){ }else{ outargs.insert("unknown",QJsonValue("unknown")); } - ret.insert("args",outargs); + //ret.insert("args",outargs); + out.out_args = outargs; }else{ //Bad/No authentication - SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); + out.CODE = RestOutputStruct::UNAUTHORIZED; + //SetOutputError(&ret, JsonValueToString(doc.object().value("id")), 401, "Unauthorized"); } - }else{ - //Error in inputs - assemble the return error message - QString id = "error"; + }else{ + //Error in inputs - assemble the return error message + out.CODE = RestOutputStruct::BADREQUEST; + /*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 - } + SetOutputError(&ret, id, 400, "Bad Request");*/ + } //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(); + //QJsonDocument retdoc; + //retdoc.setObject(ret); + //out.Body = retdoc.toJson(); out.Header << "Content-Type: text/json; charset=utf-8"; } //Return any information @@ -380,21 +390,25 @@ void WebSocket::AppCafeStatusUpdate(QString msg){ //qDebug() << "Socket Status Update:" << msg; if(!SendAppCafeEvents){ return; } //don't report events on this socket RestOutputStruct out; + out.CODE = RestOutputStruct::OK; + out.in_struct.name = "event"; + out.in_struct.namesp = "events"; //Define the output structures - QJsonObject ret; //return message + //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("")); + //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; + out.out_args = outargs; + //ret.insert("args",outargs); + //Assemble the output JSON document/text - QJsonDocument retdoc; - retdoc.setObject(ret); - out.Body = retdoc.toJson(); + //QJsonDocument retdoc; + //retdoc.setObject(ret); + //out.Body = retdoc.toJson(); out.Header << "Content-Type: text/json; charset=utf-8"; //Now send the message back through the socket if(SOCKET!=0){ SOCKET->sendTextMessage(out.assembleMessage()); }