Implement JSON API HTTP Basic authentication

jsonapi-generator is now capable of generating API for headers outside
  retroshare/ directory
jsonapi-generator do a bit of methods parameter sanity check
JsonApiServer is now integrated in the rsinit hell like other services
Add *::exportGPGKeyPairToString to a bunch of classes in cascade
RsControl is now capable of calling back a function when retroshare is almost
  completely stopped, this is useful when running retroshare toghether with
  externally managed runloop such as QCoreApplication
Expose a bunch of methods through JSON API
retroshare-nogui remove some dead code and fix stopping from the RetroShare API
This commit is contained in:
Gioacchino Mazzurco 2018-09-19 21:28:26 +02:00
parent ac9350d375
commit eb77f921ec
No known key found for this signature in database
GPG key ID: A1FBCA3872E87051
32 changed files with 816 additions and 398 deletions

View file

@ -55,7 +55,7 @@ extern RsGxsChannels* rsGxsChannels;
.Calling the JSON API with curl on the terminal
[source,bash]
--------------------------------------------------------------------------------
curl --data @paramethers.json http://127.0.0.1:9092/rsGxsChannels/createGroup
curl -u $API_USER --data @paramethers.json http://127.0.0.1:9092/rsGxsChannels/createGroup
--------------------------------------------------------------------------------
.JSON API call result
@ -76,7 +76,7 @@ least in two different ways.
.Calling the JSON API with GET method with curl on the terminal
[source,bash]
--------------------------------------------------------------------------------
curl --get --data-urlencode jsonData@paramethers.json \
curl -u $API_USER --get --data-urlencode jsonData@paramethers.json \
http://127.0.0.1:9092/rsGxsChannels/createGroup
--------------------------------------------------------------------------------
@ -85,12 +85,53 @@ equivalent to the following.
.Calling the JSON API with GET method and pre-encoded data with curl on the terminal
--------------------------------------------------------------------------------
curl http://127.0.0.1:9092/rsGxsChannels/createGroup?jsonData=%7B%0A%20%20%20%20%22group%22%3A%7B%0A%20%20%20%20%20%20%20%20%22mMeta%22%3A%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mGroupName%22%3A%22JSON%20test%20group%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mGroupFlags%22%3A4%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mSignFlags%22%3A520%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22mDescription%22%3A%22JSON%20test%20group%20description%22%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22caller_data%22%3A%22Here%20can%20go%20any%20kind%20of%20JSON%20data%20%28even%20objects%29%20that%20the%20caller%20want%20to%20get%20back%20together%20with%20the%20response%22%0A%7D
curl -u $API_USER http://127.0.0.1:9092/rsGxsChannels/createGroup?jsonData=%7B%0A%20%20%20%20%22group%22%3A%7B%0A%20%20%20%20%20%20%20%20%22mMeta%22%3A%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mGroupName%22%3A%22JSON%20test%20group%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mGroupFlags%22%3A4%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%22mSignFlags%22%3A520%0A%20%20%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%20%20%22mDescription%22%3A%22JSON%20test%20group%20description%22%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22caller_data%22%3A%22Here%20can%20go%20any%20kind%20of%20JSON%20data%20%28even%20objects%29%20that%20the%20caller%20want%20to%20get%20back%20together%20with%20the%20response%22%0A%7D
--------------------------------------------------------------------------------
Note that using +GET+ method +?jsonData=+ and then the JSON data URL encoded are
added after the path in the HTTP request.
== JSON API authentication
Most of JSON API methods require authentication as they give access to
RetroShare user data, and we don't want any application running on the system
eventually by other users be able to access private data indiscriminately.
JSON API support HTTP Basic as authentication scheme, this is enough as JSON API
server is intented for usage on the same system (127.0.0.1) not over an
untrusted network.
If you need to use JSON API over an untrusted network consider using a reverse
proxy with HTTPS such as NGINX in front of JSON API server.
If RetroShare login has been effectuated through the JSON API you can use your
location SSLID as username and your PGP password as credential for the JSON API,
but we suggests you use specific meaningful and human readable credentials for
each JSON API client so the human user can have better control over which client
can access the JSON API.
.NewToken.json
[source,json]
--------------------------------------------------------------------------------
{
"token": "myNewUser:myNewPassword"
}
--------------------------------------------------------------------------------
.An authenticated client can authorize new tokens like this
--------------------------------------------------------------------------------
curl -u $API_USER --data @NewToken.json http://127.0.0.1:9092/jsonApiServer/authorizeToken
--------------------------------------------------------------------------------
.An unauthenticated JSON API client can request access with
--------------------------------------------------------------------------------
curl --data @NewToken.json http://127.0.0.1:9092/jsonApiServer/requestNewTokenAutorization
--------------------------------------------------------------------------------
When an unauthenticated client request his token to be authorized, JSON API
server will try to ask confirmation to the human user if possible through
+mNewAccessRequestCallback+, if it is not possible or the user didn't authorized
the token +false+ is returned.
== Offer new RetroShare services through JSON API
To offer a retroshare service through the JSON API, first of all one need find
@ -231,6 +272,17 @@ data: {"results":[{"size":668,"hash":"e8845280912ebf3779e400000000000000000000",
--------------------------------------------------------------------------------
By default JSON API methods requires client authentication and their wrappers
are automatically generated by +json-api-generator+.
In some cases methods need do be accessible without authentication such as
+rsLoginHelper/getLocations+ so in the doxygen documentaion they have the custom
command +@jsonapi{RS_VERSION,unauthenticated}+.
Other methods such as +/rsControl/rsGlobalShutDown+ need special care so they
are marked with the custom doxygen command +@jsonapi{RS_VERSION,manualwrapper}+
and their wrappers are not automatically generated but written manually into
+JsonApiServer::JsonApiServer(...)+.
== A bit of history
=== First writings about this

View file

@ -61,5 +61,5 @@ $%outputParamsSerialization%$
session->yield(message.str());
$%sessionDelayedClose%$
} );
});
}, $%requiresAuth%$);

View file

@ -2,6 +2,7 @@ DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "libretroshare"
ALIASES += jsonapi{1}="\xmlonly<jsonapi minversion=\"\1\"/>\endxmlonly"
ALIASES += jsonapi{2}="\xmlonly<jsonapi minversion=\"\1\" access=\"\2\"/>\endxmlonly"
# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
# documentation from any documented member that it re-implements.

View file

@ -22,6 +22,8 @@
#include <QDirIterator>
#include <QFileInfo>
#include <iterator>
#include <functional>
#include <QVariant>
struct MethodParam
{
@ -40,7 +42,10 @@ struct MethodParam
int main(int argc, char *argv[])
{
if(argc != 3)
qFatal("Usage: jsonapi-generator SOURCE_PATH OUTPUT_PATH");
{
qDebug() << "Usage: jsonapi-generator SOURCE_PATH OUTPUT_PATH";
return EINVAL;
}
QString sourcePath(argv[1]);
QString outputPath(argv[2]);
@ -59,6 +64,20 @@ int main(int argc, char *argv[])
qFatal(QString("Can't open: " + cppApiIncludesFilePath).toLatin1().data());
QSet<QString> cppApiIncludesSet;
auto fatalError = [&](
std::initializer_list<QVariant> errors, int ernum = EINVAL )
{
QString errorMsg;
for(const QVariant& error: errors)
errorMsg += error.toString() + " ";
errorMsg.chop(1);
QByteArray cppError(QString("#error "+errorMsg+"\n").toLocal8Bit());
wrappersDefFile.write(cppError);
cppApiIncludesFile.write(cppError);
qDebug() << errorMsg;
return ernum;
};
QDirIterator it(doxPrefix, QStringList() << "*8h.xml", QDir::Files);
while(it.hasNext())
{
@ -74,9 +93,10 @@ int main(int argc, char *argv[])
continue;
}
QFileInfo hfi(hFile);
QString headerFileName(hfi.fileName());
headerFileName.replace(QString("_8h.xml"), QString(".h"));
QFileInfo headerFileInfo(hDoc.elementsByTagName("location").at(0)
.attributes().namedItem("file").nodeValue());
QString headerRelPath = headerFileInfo.dir().dirName() + "/" +
headerFileInfo.fileName();
QDomNodeList sectiondefs = hDoc.elementsByTagName("memberdef");
for(int j = 0; j < sectiondefs.size(); ++j)
@ -112,6 +132,7 @@ int main(int argc, char *argv[])
QDomNode member = members.item(i);
QString refid(member.attributes().namedItem("refid").nodeValue());
QString methodName(member.firstChildElement("name").toElement().text());
bool requiresAuth = true;
QString defFilePath(doxPrefix + refid.split('_')[0] + ".xml");
qDebug() << "Looking for" << typeName << methodName << "into"
@ -134,10 +155,21 @@ int main(int argc, char *argv[])
QDomElement tmpMBD = memberdefs.item(k).toElement();
QString tmpId = tmpMBD.attributes().namedItem("id").nodeValue();
QString tmpKind = tmpMBD.attributes().namedItem("kind").nodeValue();
bool hasJsonApi = !tmpMBD.elementsByTagName("jsonapi").isEmpty();
if( tmpId == refid && tmpKind == "function" && hasJsonApi )
QDomNodeList tmpJsonApiTagList(tmpMBD.elementsByTagName("jsonapi"));
if( tmpJsonApiTagList.count() && tmpId == refid &&
tmpKind == "function")
{
memberdef = tmpMBD;
QDomElement tmpJsonApiTag = tmpJsonApiTagList.item(0).toElement();
QString tmpAccessValue;
if(tmpJsonApiTag.hasAttribute("access"))
tmpAccessValue = tmpJsonApiTag.attributeNode("access").nodeValue();
requiresAuth = "unauthenticated" != tmpAccessValue;
if("manualwrapper" != tmpAccessValue)
memberdef = tmpMBD;
break;
}
}
@ -189,6 +221,7 @@ int main(int argc, char *argv[])
pType.replace(QString("&"), QString());
pType.replace(QString(" "), QString());
}
paramsMap.insert(tmpParam.name, tmpParam);
orderedParamNames.push_back(tmpParam.name);
}
@ -211,6 +244,16 @@ int main(int argc, char *argv[])
}
}
// Params sanity check
for( const MethodParam& pm : paramsMap)
if( !(pm.isMultiCallback || pm.isSingleCallback
|| pm.in || pm.out) )
return fatalError(
{ "Parameter:", pm.name, "of:", apiPath,
"declared in:", headerRelPath,
"miss doxygen parameter direction attribute!",
defFile.fileName() });
QString functionCall("\t\t");
if(retvalType != "void")
{
@ -325,7 +368,6 @@ int main(int argc, char *argv[])
substitutionsMap.insert("inputParamsDeserialization", inputParamsDeserialization);
substitutionsMap.insert("outputParamsSerialization", outputParamsSerialization);
substitutionsMap.insert("instanceName", instanceName);
substitutionsMap.insert("headerFileName", headerFileName);
substitutionsMap.insert("functionCall", functionCall);
substitutionsMap.insert("apiPath", apiPath);
substitutionsMap.insert("sessionEarlyClose", sessionEarlyClose);
@ -334,6 +376,7 @@ int main(int argc, char *argv[])
substitutionsMap.insert("callbackName", callbackName);
substitutionsMap.insert("callbackParams", callbackParams);
substitutionsMap.insert("callbackParamsSerialization", callbackParamsSerialization);
substitutionsMap.insert("requiresAuth", requiresAuth ? "true" : "false");
QString templFilePath(sourcePath);
if(hasMultiCallback || hasSingleCallback)
@ -351,7 +394,7 @@ int main(int argc, char *argv[])
wrappersDefFile.write(wrapperDef.toLocal8Bit());
cppApiIncludesSet.insert("#include \"retroshare/" + headerFileName + "\"\n");
cppApiIncludesSet.insert("#include \"" + headerRelPath + "\"\n");
}
}
}

View file

@ -44,5 +44,5 @@ $%outputParamsSerialization%$
// return them to the API caller
DEFAULT_API_CALL_JSON_RETURN(rb::OK);
} );
});
}, $%requiresAuth%$);