--- a
+++ b/src/ohcredentials.cxx
@@ -0,0 +1,480 @@
+/* Copyright (C) 2018 J.F.Dockes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the
+ * Free Software Foundation, Inc.,
+ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#define LOGGER_LOCAL_LOGINC 6
+
+#include "ohcredentials.hxx"
+
+#include <upnp/upnp.h>
+
+#include <functional>
+#include <iostream>
+#include <map>
+#include <utility>
+#include <vector>
+
+#include "conftree.h"
+#include "main.hxx"
+#include "pathut.h"
+#include "execmd.h"
+#include "mediaserver/cdplugins/cmdtalk.h"
+
+#include "libupnpp/log.hxx"
+#include "libupnpp/base64.hxx"
+#include "libupnpp/soaphelp.hxx"
+#include "libupnpp/device/device.hxx"
+
+using namespace std;
+using namespace std::placeholders;
+
+static const string sTpCredentials("urn:av-openhome-org:service:Credentials:1");
+static const string sIdCredentials("urn:av-openhome-org:serviceId:Credentials");
+
+static const string idstring{"tidalhifi.com qobuz.com"};
+static const map<string, string> idmap {
+ {"tidalhifi.com", "tidal"},
+ {"qobuz.com", "qobuz"}
+};
+
+// Should be derived into ServiceCredsQobuz, ServiceCredsTidal, there
+// is a lot in common and a few diffs.
+struct ServiceCreds {
+ ServiceCreds() {}
+ ServiceCreds(const string& inm, const string& u, const string& p,
+ const string& ep)
+ : servicename(inm), user(u), password(p), encryptedpass(ep) {
+ // The appid used by the Qobuz python module. Has to be
+ // consistent with the token obtained by the same, so we
+ // return it (by convention, as seen in wiresharking kazoo) in
+ // the data field. Of course, this class should be derived
+ // into service-specific ones, and this should happen only for
+ // the qobuz version. We could and do obtain the appid from
+ // the module, but kazoo wants it before we login, so just
+ // hard-code it for now.
+ data = "285473059";
+ }
+ ~ServiceCreds() {
+ delete cmd;
+ }
+
+ bool maybeStartCmd() {
+ LOGDEB("ServiceCreds: " << servicename << " maybeStartCmd()\n");
+ if (nullptr == cmd) {
+ cmd = new CmdTalk(30);
+ }
+ if (cmd->running()) {
+ return true;
+ }
+ LOGDEB("ServiceCreds: " << servicename << " starting cmd\n");
+ string exepath = path_cat(g_datadir, "cdplugins");
+ exepath = path_cat(exepath, servicename);
+ exepath = path_cat(exepath, servicename + "-app" + ".py");
+
+ string pythonpath = string("PYTHONPATH=") +
+ path_cat(g_datadir, "cdplugins") + ":" +
+ path_cat(g_datadir, "cdplugins/pycommon") + ":" +
+ path_cat(g_datadir, "cdplugins/" + servicename);
+ if (!cmd->startCmd(exepath, {/*args*/},
+ /* env */ {pythonpath})) {
+ LOGERR("ServiceCreds::maybeStartCmd: startCmd failed\n");
+ return false;
+ }
+ LOGDEB("ServiceCreds: " << servicename << " cmd started\n");
+ return true;
+ }
+
+ string login() {
+ LOGDEB("ServiceCreds: " << servicename << " login\n");
+ if (!token.empty()) {
+ return token;
+ }
+ if (!maybeStartCmd()) {
+ return string();
+ }
+ unordered_map<string,string> res;
+ if (!cmd->callproc("login", {{"user", user},
+ {"password", password}}, res)) {
+ LOGERR("ServiceCreds::login: slave failure. Service " <<
+ servicename << " user " << user << endl);
+ return string();
+ }
+ auto it = res.find("appid");
+ if (it == res.end()) {
+ LOGERR("ServiceCreds::login: no appid. Service " <<
+ servicename << " user " << user << endl);
+ return string();
+ }
+ appid = it->second;
+ data = appid;
+ it = res.find("token");
+ if (it == res.end()) {
+ LOGERR("ServiceCreds::login: no token. Service " <<
+ servicename << " user " << user << endl);
+ return string();
+ }
+ return token = it->second;
+ }
+
+ void logout() {
+ appid.clear();
+ token.clear();
+ }
+
+ string str() {
+ string s;
+ s += "Service: " + servicename + " User: " + user +
+ " Pass: " + password + " Appid " + appid + " Token " + token +
+ " EPass: " + encryptedpass + " Enabled: " +
+ SoapHelp::i2s(enabled) + " Status: " + status + " Data: " + data;
+ return s;
+ }
+
+ // Internal name, like "qobuz"
+ string servicename;
+ string user;
+ string password;
+ string encryptedpass;
+ string appid;
+ string token;
+ bool enabled{true};
+ string status;
+ // For qobuz, data contains the "app id" 854233864 for ohplayer.
+ // XBMC is 285473059
+ string data;
+ CmdTalk *cmd{0};
+};
+
+class OHCredentials::Internal {
+public:
+
+ Internal(const string& cd) {
+ string cachedir = path_cat(cd, "ohcreds");
+ if (!path_makepath(cachedir, 0700)) {
+ LOGERR("OHCredentials: can't create cache dir " << cachedir <<endl);
+ return;
+ }
+ keyfile = path_cat(cachedir, "credkey.pem");
+ cmd.putenv("RANDFILE", path_cat(cachedir, "randfile"));
+
+ if (!path_exists(keyfile)) {
+ vector<string> acmd{"openssl", "genrsa", "-out", keyfile, "4096"};
+ int status = cmd.doexec1(acmd);
+ if (status != 0) {
+ LOGERR("OHCredentials: could not create key\n");
+ return;
+ }
+ }
+
+ vector<string> acmd{"openssl", "pkey", "-in", keyfile, "-pubout"};
+ if (!cmd.backtick(acmd, pubkey)) {
+ LOGERR("OHCredentials: could not read public key\n");
+ return;
+ }
+
+ LOGDEB("OHCredentials: my public key:\n" << pubkey << endl);
+ }
+
+ bool decrypt(const string& in, string& out) {
+ vector<string> acmd{"openssl", "pkeyutl", "-inkey",
+ keyfile, "-pkeyopt", "rsa_padding_mode:oaep", "-decrypt"};
+ int status = cmd.doexec1(acmd, &in, &out);
+ if (status) {
+ LOGERR("OHCredentials: decrypt failed\n");
+ return false;
+ }
+ LOGDEB("decrypt: [" << out << "]\n");
+ return true;
+ }
+
+ bool setEnabled(const string& id, bool enabled) {
+ auto it = creds.find(id);
+ if (it == creds.end()) {
+ return false;
+ }
+ it->second.enabled = enabled;
+ return true;
+ }
+
+ ExecCmd cmd;
+ string keyfile;
+ string pubkey;
+ int seq{1};
+ map<string, ServiceCreds> creds;
+};
+
+
+OHCredentials::OHCredentials(UpMpd *dev, const string& cachedir)
+ : OHService(sTpCredentials, sIdCredentials, dev), m(new Internal(cachedir))
+{
+ dev->addActionMapping(
+ this, "Set",
+ bind(&OHCredentials::actSet, this, _1, _2));
+ dev->addActionMapping(
+ this, "Clear",
+ bind(&OHCredentials::actClear, this, _1, _2));
+ dev->addActionMapping(
+ this, "SetEnabled",
+ bind(&OHCredentials::actSetEnabled, this, _1, _2));
+ dev->addActionMapping(
+ this, "Get",
+ bind(&OHCredentials::actGet, this, _1, _2));
+ dev->addActionMapping(
+ this, "Login",
+ bind(&OHCredentials::actLogin, this, _1, _2));
+ dev->addActionMapping(
+ this, "ReLogin",
+ bind(&OHCredentials::actReLogin, this, _1, _2));
+ dev->addActionMapping(
+ this, "GetIds",
+ bind(&OHCredentials::actGetIds, this, _1, _2));
+ dev->addActionMapping(
+ this, "GetPublicKey",
+ bind(&OHCredentials::actGetPublicKey, this, _1, _2));
+ dev->addActionMapping(
+ this, "GetSequenceNumber",
+ bind(&OHCredentials::actGetSequenceNumber, this, _1, _2));
+}
+
+OHCredentials::~OHCredentials()
+{
+ delete m;
+}
+
+bool OHCredentials::makestate(unordered_map<string, string> &st)
+{
+ st.clear();
+ if (nullptr == m) {
+ return false;
+ }
+ st["Ids"] = idstring;
+ st["PublicKey"] = m->pubkey;
+ st["SequenceNumber"] = SoapHelp::i2s(m->seq);
+ return true;
+}
+
+int OHCredentials::actSet(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actSet: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ std::string in_UserName;
+ ok = sc.get("UserName", &in_UserName);
+ if (!ok) {
+ LOGERR("OHCredentials::actSet: no UserName in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ string in_Password;
+ ok = sc.get("Password", &in_Password);
+ if (!ok) {
+ LOGERR("OHCredentials::actSet: no Password in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actSet: " << " Id " << in_Id << " UserName " <<
+ in_UserName << " Password " << in_Password << endl);
+
+ const auto it1 = idmap.find(in_Id);
+ if (it1 == idmap.end()) {
+ LOGERR("OHCredentials::actSet: bad service id [" << in_Id <<"]\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ string servicename = it1->second;
+ string cpass = base64_decode(in_Password);
+ string plainpass;
+ if (!m->decrypt(cpass, plainpass)) {
+ LOGERR("OHCredentials::actSet: could not decrypt\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ auto it = m->creds.find(in_Id);
+ if (it == m->creds.end() || it->second.user != in_UserName ||
+ it->second.password != plainpass ||
+ it->second.encryptedpass != in_Password) {
+ m->creds[in_Id] = ServiceCreds(servicename, in_UserName, plainpass,
+ in_Password);
+ }
+ m->seq++;
+
+ if (m->setEnabled(in_Id, true)) {
+ return UPNP_E_SUCCESS;
+ } else {
+ return UPNP_E_INVALID_PARAM;
+ }
+}
+
+int OHCredentials::actLogin(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actLogin: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actLogin: " << " Id " << in_Id << endl);
+ auto it = m->creds.find(in_Id);
+ if (it == m->creds.end()) {
+ LOGERR("OHCredentials::Login: Id " << in_Id << " not found\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ string token = it->second.login();
+ LOGDEB("OHCredentials::Login: got token " << token << endl);
+ data.addarg("Token", token);
+ m->seq++;
+ return UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actReLogin(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actReLogin: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ std::string in_CurrentToken;
+ ok = sc.get("CurrentToken", &in_CurrentToken);
+ if (!ok) {
+ LOGERR("OHCredentials::actReLogin: no CurrentToken in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actReLogin: " << " Id " << in_Id << " CurrentToken "
+ << in_CurrentToken << endl);
+
+ auto it = m->creds.find(in_Id);
+ if (it == m->creds.end()) {
+ LOGERR("OHCredentials::Login: Id " << in_Id << " not found\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ it->second.logout();
+ string token = it->second.login();
+ data.addarg("NewToken", token);
+ m->seq++;
+ return UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actClear(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actClear: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actClear: " << " Id " << in_Id << endl);
+ if (idmap.find(in_Id) == idmap.end()) {
+ LOGERR("OHCredentials::actClear: bad service id [" << in_Id <<"]\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ auto it = m->creds.find(in_Id);
+ if (it != m->creds.end()) {
+ m->creds.erase(it);
+ m->seq++;
+ }
+ return UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actSetEnabled(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actSetEnabled: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+ bool in_Enabled;
+ ok = sc.get("Enabled", &in_Enabled);
+ if (!ok) {
+ LOGERR("OHCredentials::actSetEnabled: no Enabled in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actSetEnabled: " << " Id " << in_Id << " Enabled " <<
+ in_Enabled << endl);
+ if (m->setEnabled(in_Id, in_Enabled)) {
+ m->seq++;
+ return UPNP_E_SUCCESS;
+ } else {
+ return UPNP_E_INVALID_PARAM;
+ }
+}
+
+int OHCredentials::actGet(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ bool ok = false;
+ std::string in_Id;
+ ok = sc.get("Id", &in_Id);
+ if (!ok) {
+ LOGERR("OHCredentials::actGet: no Id in params\n");
+ return UPNP_E_INVALID_PARAM;
+ }
+
+ LOGDEB("OHCredentials::actGet: " << " Id " << in_Id << endl);
+
+ auto it = m->creds.find(in_Id);
+ ServiceCreds emptycreds;
+ ServiceCreds *credsp(&emptycreds);
+ if (it != m->creds.end()) {
+ credsp = &(it->second);
+ } else {
+ LOGDEB("OHCredentials::actGet: nothing found for " << in_Id << endl);
+ }
+ LOGDEB("OHCredentials::actGet: data for " << in_Id << " " <<
+ credsp->str() << endl);
+ data.addarg("UserName", credsp->user);
+ // Encrypted password !
+ data.addarg("Password", credsp->encryptedpass);
+ // In theory enabled is set in response to setEnabled() or
+ // set(). In practise, if it is not set, we don't get to the qobuz
+ // settings screen in kazoo.
+ data.addarg("Enabled", credsp->enabled ? "1" : "1");
+ data.addarg("Status", credsp->status);
+ data.addarg("Data", credsp->data);
+ return UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actGetIds(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ LOGDEB("OHCredentials::actGetIds: " << endl);
+ data.addarg("Ids", idstring);
+ return UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actGetPublicKey(const SoapIncoming& sc, SoapOutgoing& data)
+{
+ LOGDEB("OHCredentials::actGetPublicKey: " << endl);
+ data.addarg("PublicKey", m->pubkey);
+ return m->pubkey.empty() ? UPNP_E_INTERNAL_ERROR : UPNP_E_SUCCESS;
+}
+
+int OHCredentials::actGetSequenceNumber(const SoapIncoming& sc,
+ SoapOutgoing& data)
+{
+ LOGDEB("OHCredentials::actGetSequenceNumber: " << endl);
+ data.addarg("SequenceNumber", SoapHelp::i2s(m->seq));
+ return UPNP_E_SUCCESS;
+}