--- 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;
+}