Merge pull request #1349 from G10h4ck/jsonapi

Implement JSON API HTTP Basic authentication
This commit is contained in:
G10h4ck 2018-09-25 18:10:16 +02:00 committed by GitHub
commit f09bef2ac8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1302 additions and 425 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 <QtCore/QDirIterator>
#include <QtCore/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%$);