a/src/ohcredentials.cxx b/src/ohcredentials.cxx
...
...
26
#include <functional>
26
#include <functional>
27
#include <iostream>
27
#include <iostream>
28
#include <map>
28
#include <map>
29
#include <utility>
29
#include <utility>
30
#include <vector>
30
#include <vector>
31
#include <regex>
31
32
32
#include "conftree.h"
33
#include "conftree.h"
33
#include "main.hxx"
34
#include "main.hxx"
34
#include "pathut.h"
35
#include "pathut.h"
35
#include "execmd.h"
36
#include "execmd.h"
...
...
37
38
38
#include "libupnpp/log.hxx"
39
#include "libupnpp/log.hxx"
39
#include "libupnpp/base64.hxx"
40
#include "libupnpp/base64.hxx"
40
#include "libupnpp/soaphelp.hxx"
41
#include "libupnpp/soaphelp.hxx"
41
#include "libupnpp/device/device.hxx"
42
#include "libupnpp/device/device.hxx"
43
#include "mediaserver/cdplugins/cdplugin.hxx"
42
44
43
using namespace std;
45
using namespace std;
44
using namespace std::placeholders;
46
using namespace std::placeholders;
45
47
46
static const string sTpCredentials("urn:av-openhome-org:service:Credentials:1");
48
static const string sTpCredentials("urn:av-openhome-org:service:Credentials:1");
...
...
50
static const map<string, string> idmap {
52
static const map<string, string> idmap {
51
    {"tidalhifi.com", "tidal"},
53
    {"tidalhifi.com", "tidal"},
52
    {"qobuz.com", "qobuz"}
54
    {"qobuz.com", "qobuz"}
53
};
55
};
54
56
55
// Should be derived into ServiceCredsQobuz, ServiceCredsTidal, there
57
// This is used for translating urls for the special use of
58
// Kazoo/Lumin. The media server, which is used to run the http server
59
// and for getting the real media URLs, must run on this host (for one
60
// thing the creds are passed through a local file).
61
// *** Note that this needs xxxautostart to work, else the HTTP server
62
//     won't be listening (as long as nobody accesses the app section
63
//     of the media server) ***
64
static string upnphost;
65
66
// Called from OHPlaylist. The CP (Kazoo/Lumin mostly) will send URIs
67
// like qobuz:// tidal:// and expect the renderer to know what to do
68
// with them. We transform them so that they point to our media server
69
// gateway (which should be running of course for this to work).
70
bool OHCredsMaybeMorphSpecialUri(string& uri)
71
{
72
    if (uri.find("http://") == 0 || uri.find("https://") == 0) {
73
        return true;
74
    }
75
76
    static string sport;
77
    if (sport.empty()) {
78
        std::unique_lock<std::mutex>(g_configlock);
79
        int port = CDPluginServices::default_microhttpport();
80
        if (!g_config->get("plgmicrohttpport", sport)) {
81
            sport = SoapHelp::i2s(port);
82
        }
83
    }
84
85
    // http://wiki.openhome.org/wiki/Av:Developer:Eriskay:StreamingServices
86
    // Tidal and qobuz tracks added by Kazoo / Lumin: 
87
    //   tidal://track?version=1&trackId=[tidal_track_id]
88
    //   qobuz://track?version=2&trackId=[qobuz_track_id]
89
    
90
    string se =
91
        "(tidal|qobuz)://track\\?version=([[:digit:]]+)&trackId=([[:digit:]]+)";
92
    std::regex e(se);
93
    std::smatch mr;
94
    bool found = std::regex_match(uri, mr, e);
95
    if (found) {
96
        string pathprefix = CDPluginServices::getpathprefix(mr[1]);
97
98
        // The microhttpd code actually only cares about getting a
99
        // trackId parameter. Make it look what the plugins normally
100
        // generate anyway:
101
        string path = path_cat(pathprefix,
102
                               "track?version=1&trackId=" + mr[3].str());
103
        uri = string("http://") + upnphost + ":" + sport + path;
104
    }
105
    return found;
106
}
107
108
// We might want to derive this into ServiceCredsQobuz,
56
// is a lot in common and a few diffs.
109
// ServiceCredsTidal, there is a lot in common and a few diffs.
57
struct ServiceCreds {
110
struct ServiceCreds {
58
    ServiceCreds() {}
111
    ServiceCreds() {}
59
    ServiceCreds(const string& inm, const string& u, const string& p,
112
    ServiceCreds(const string& inm, const string& u, const string& p,
60
                 const string& ep)
113
                 const string& ep)
61
        : servicename(inm), user(u), password(p), encryptedpass(ep) {
114
        : servicename(inm), user(u), password(p), encryptedpass(ep) {
115
116
        if (servicename == "qobuz") {
62
        // The appid used by the Qobuz python module. Has to be
117
            // The appid used by the Qobuz python module. Has to be
63
        // consistent with the token obtained by the same, so we
118
            // consistent with the token obtained by the same, so we
64
        // return it (by convention, as seen in wiresharking kazoo) in
119
            // return it (by convention, as seen in wiresharking
65
        // the data field. Of course, this class should be derived
120
            // kazoo) in the data field. We could and do obtain the
66
        // into service-specific ones, and this should happen only for
121
            // appid from the module, but kazoo apparently wants it
67
        // the qobuz version. We could and do obtain the appid from
122
            // before we login, so just hard-code it for now.  The
68
        // the module, but kazoo wants it before we login, so just
123
            // Python code uses the value from XBMC,285473059,
69
        // hard-code it for now.
124
            // ohplayer uses 854233864
70
        data = "285473059";
125
            data = "285473059";
126
        } else if (servicename == "tidal") {
127
            // data contains the country code
128
            data = "FR";
71
    }
129
        }
130
    }
131
72
    ~ServiceCreds() {
132
    ~ServiceCreds() {
73
        delete cmd;
133
        delete cmd;
74
    }
134
    }
75
135
136
    // We need a Python helper to perform the login. That's the media
137
    // server gateway module, from which we only use a separate method
138
    // which logs-in and returns the auth data (token, etc.)
76
    bool maybeStartCmd() {
139
    bool maybeStartCmd() {
77
        LOGDEB("ServiceCreds: " << servicename << " maybeStartCmd()\n");
140
        LOGDEB("ServiceCreds: " << servicename << " maybeStartCmd()\n");
78
        if (nullptr == cmd) {
141
        if (nullptr == cmd) {
79
            cmd = new CmdTalk(30);
142
            cmd = new CmdTalk(30);
80
        }
143
        }
...
...
88
151
89
        string pythonpath = string("PYTHONPATH=") +
152
        string pythonpath = string("PYTHONPATH=") +
90
            path_cat(g_datadir, "cdplugins") + ":" +
153
            path_cat(g_datadir, "cdplugins") + ":" +
91
            path_cat(g_datadir, "cdplugins/pycommon") + ":" +
154
            path_cat(g_datadir, "cdplugins/pycommon") + ":" +
92
            path_cat(g_datadir, "cdplugins/" + servicename);
155
            path_cat(g_datadir, "cdplugins/" + servicename);
156
        string configname = string("UPMPD_CONFIG=") + g_configfilename;
157
        // hostport is not needed by this login-only instance.
158
        string hostport = string("UPMPD_HTTPHOSTPORT=bogus:0");
159
        string pp = string("UPMPD_PATHPREFIX=") +
160
            CDPluginServices::getpathprefix(servicename);
93
        if (!cmd->startCmd(exepath, {/*args*/},
161
        if (!cmd->startCmd(exepath, {/*args*/},
94
                          /* env */ {pythonpath})) {
162
                           /* env */ {pythonpath, configname, hostport, pp})) {
95
            LOGERR("ServiceCreds::maybeStartCmd: startCmd failed\n");
163
            LOGERR("ServiceCreds::maybeStartCmd: startCmd failed\n");
96
            return false;
164
            return false;
97
        }
165
        }
98
        LOGDEB("ServiceCreds: " << servicename << " cmd started\n");
166
        LOGDEB("ServiceCreds: " << servicename << " cmd started\n");
99
        return true;
167
        return true;
100
    }
168
    }
101
169
102
    string login() {
170
    string login() {
103
        LOGDEB("ServiceCreds: " << servicename << " login\n");
171
        LOGDEB("ServiceCreds: " << servicename << " login\n");
172
173
        // Check if already logged-in
174
        if (servicename == "qobuz" || servicename == "tidal") {
104
        if (!token.empty()) {
175
            if (!servicedata["token"].empty()) {
176
                return servicedata["token"];
177
            }
178
        } else {
179
            LOGERR("Unsupported service: " << servicename << endl);
105
            return token;
180
            return string();
106
        }
181
        }
182
107
        if (!maybeStartCmd()) {
183
        if (!maybeStartCmd()) {
108
            return string();
184
            return string();
109
        }
185
        }
110
        unordered_map<string,string> res;
186
        unordered_map<string,string> res;
111
        if (!cmd->callproc("login", {{"user", user},
187
        if (!cmd->callproc("login", {{"user", user},
112
                    {"password", password}}, res)) {
188
                    {"password", password}}, res)) {
113
            LOGERR("ServiceCreds::login: slave failure. Service " <<
189
            LOGERR("ServiceCreds::login: slave failure. Service " <<
114
                   servicename << " user " << user << endl);
190
                   servicename << " user " << user << endl);
115
            return string();
191
            return string();
116
        }
192
        }
193
194
        vector<string> toknames;
195
        if (servicename == "qobuz") {
196
            toknames = vector<string>{"token", "appid"};
197
        } else if (servicename == "tidal") {
198
            toknames = vector<string>{"token", "country"};
199
        }
200
        for (const auto& toknm : toknames) {
117
        auto it = res.find("appid");
201
            auto it = res.find(toknm);
118
        if (it == res.end()) {
202
            if (it == res.end()) {
119
            LOGERR("ServiceCreds::login: no appid. Service " <<
203
                LOGERR("ServiceCreds::login: no " << toknm << ". Service " <<
120
                   servicename << " user " << user << endl);
204
                       servicename << " user " << user << endl);
121
            return string();
205
                return string();
122
        }
206
            }
123
        appid = it->second;
124
        data = appid;
125
        it = res.find("token");
126
        if (it == res.end()) {
127
            LOGERR("ServiceCreds::login: no token. Service " <<
128
                   servicename << " user " << user << endl);
129
            return string();
130
        }
131
        return token = it->second;
207
            servicedata[toknm] = it->second;
208
        }
209
        if (servicename == "qobuz") {
210
            data = servicedata["appid"];
211
        } else if (servicename == "tidal") {
212
            data = servicedata["country"];
213
        }
214
        return servicedata["token"];
132
    }
215
    }
133
216
134
    void logout() {
217
    void logout() {
135
        appid.clear();
218
        servicedata.clear();
136
        token.clear();
137
    }
219
    }
138
220
139
    string str() {
221
    string str() {
140
        string s;
222
        string s;
223
        string sdata;
224
        for (const auto& entry:servicedata) {
225
            sdata += entry.first + " : " + entry.second + ", ";
226
        }
141
        s += "Service: " + servicename + " User: " + user +
227
        s += "Service: " + servicename + " User: " + user +
142
            /*" Pass: "+password*/ + " Appid " + appid + " Token " + token +
228
            /*" Pass: "+password*/ + " Servicedata: " + sdata +
143
            /*" EPass: "+encryptedpass*/ + " Enabled: " +
229
            /*" EPass: "+encryptedpass*/ + " Enabled: " +
144
            SoapHelp::i2s(enabled) + " Status: " + status + " Data: " + data;
230
            SoapHelp::i2s(enabled) + " Status: " + status + " Data: " + data;
145
        return s;
231
        return s;
146
    }
232
    }
147
233
148
    // Internal name, like "qobuz"
234
    // Internal name, like "qobuz"
149
    string servicename;
235
    string servicename;
150
    string user;
236
    string user;
151
    string password;
237
    string password;
152
    string encryptedpass;
238
    string encryptedpass;
153
    string appid;
154
    string token;
155
    bool enabled{true};
239
    bool enabled{true};
240
    CmdTalk *cmd{0};
241
    // Things we obtain from the module and send to the CP
242
    unordered_map<string,string> servicedata;
243
156
    string status;
244
    string status;
157
    // For qobuz, data contains the "app id" 854233864 for ohplayer.
245
    // See comments about 'data' use above.
158
    // XBMC is 285473059
159
    string data;
246
    string data;
160
    CmdTalk *cmd{0};
161
};
247
};
162
248
163
class OHCredentials::Internal {
249
class OHCredentials::Internal {
164
public:
250
public:
165
    
251
    
...
...
212
        return true;
298
        return true;
213
    }
299
    }
214
300
215
    bool save() {
301
    bool save() {
216
        string credsfile = path_cat(cachedir, "screds");
302
        string credsfile = path_cat(cachedir, "screds");
217
        FILE *fp = fopen(credsfile.c_str(), "w");
303
        ConfSimple credsconf(credsfile.c_str());
218
        fchmod(fileno(fp), 0600);
304
        if (!credsconf.ok()) {
219
        if (nullptr == fp) {
220
            LOGERR("OHCredentials: error opening " << credsfile <<
305
            LOGERR("OHCredentials: error opening " << credsfile <<
221
                   " errno " << errno << endl);
306
                   " errno " << errno << endl);
222
            return false;
307
            return false;
223
        }
308
        }
224
        for (const auto& cred : creds) {
309
        for (const auto& cred : creds) {
225
            fprintf(fp, "[%s]\nu=%s\np=%s\n", cred.second.servicename.c_str(),
310
            credsconf.set("u", cred.second.user, cred.second.servicename);
226
                    cred.second.user.c_str(), cred.second.password.c_str());
311
            credsconf.set("p", cred.second.password, cred.second.servicename);
227
        }
312
        }
228
        fclose(fp);
313
        chmod(credsfile.c_str(), 0600);
229
        return true;
314
        return true;
230
    }
315
    }
231
    
316
    
232
    ExecCmd cmd;
317
    ExecCmd cmd;
233
    string cachedir;
318
    string cachedir;
...
...
266
        this, "GetPublicKey",
351
        this, "GetPublicKey",
267
        bind(&OHCredentials::actGetPublicKey, this, _1, _2));
352
        bind(&OHCredentials::actGetPublicKey, this, _1, _2));
268
    dev->addActionMapping(
353
    dev->addActionMapping(
269
        this, "GetSequenceNumber",
354
        this, "GetSequenceNumber",
270
        bind(&OHCredentials::actGetSequenceNumber, this, _1, _2));
355
        bind(&OHCredentials::actGetSequenceNumber, this, _1, _2));
356
357
    unsigned short usport;
358
    dev->ipv4(&upnphost, &usport);
271
}
359
}
272
360
273
OHCredentials::~OHCredentials()
361
OHCredentials::~OHCredentials()
274
{
362
{
275
    delete m;
363
    delete m;