Switch to unified view

a b/src/ohcredentials.cxx
1
/* Copyright (C) 2018 J.F.Dockes
2
 *
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program; if not, write to the
15
 * Free Software Foundation, Inc.,
16
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17
 */
18
19
#define LOGGER_LOCAL_LOGINC 6
20
21
#include "ohcredentials.hxx"
22
23
#include <upnp/upnp.h>
24
25
#include <functional>
26
#include <iostream>
27
#include <map>
28
#include <utility>
29
#include <vector>
30
31
#include "conftree.h"
32
#include "main.hxx"
33
#include "pathut.h"
34
#include "execmd.h"
35
#include "mediaserver/cdplugins/cmdtalk.h"
36
37
#include "libupnpp/log.hxx"
38
#include "libupnpp/base64.hxx"
39
#include "libupnpp/soaphelp.hxx"
40
#include "libupnpp/device/device.hxx"
41
42
using namespace std;
43
using namespace std::placeholders;
44
45
static const string sTpCredentials("urn:av-openhome-org:service:Credentials:1");
46
static const string sIdCredentials("urn:av-openhome-org:serviceId:Credentials");
47
48
static const string idstring{"tidalhifi.com qobuz.com"};
49
static const map<string, string> idmap {
50
    {"tidalhifi.com", "tidal"},
51
    {"qobuz.com", "qobuz"}
52
};
53
54
// Should be derived into ServiceCredsQobuz, ServiceCredsTidal, there
55
// is a lot in common and a few diffs.
56
struct ServiceCreds {
57
    ServiceCreds() {}
58
    ServiceCreds(const string& inm, const string& u, const string& p,
59
                 const string& ep)
60
        : servicename(inm), user(u), password(p), encryptedpass(ep) {
61
        // The appid used by the Qobuz python module. Has to be
62
        // consistent with the token obtained by the same, so we
63
        // return it (by convention, as seen in wiresharking kazoo) in
64
        // the data field. Of course, this class should be derived
65
        // into service-specific ones, and this should happen only for
66
        // the qobuz version. We could and do obtain the appid from
67
        // the module, but kazoo wants it before we login, so just
68
        // hard-code it for now.
69
        data = "285473059";
70
    }
71
    ~ServiceCreds() {
72
        delete cmd;
73
    }
74
75
    bool maybeStartCmd() {
76
        LOGDEB("ServiceCreds: " << servicename << " maybeStartCmd()\n");
77
        if (nullptr == cmd) {
78
            cmd = new CmdTalk(30);
79
        }
80
        if (cmd->running()) {
81
            return true;
82
        }
83
        LOGDEB("ServiceCreds: " << servicename << " starting cmd\n");
84
        string exepath = path_cat(g_datadir, "cdplugins");
85
        exepath = path_cat(exepath, servicename);
86
        exepath = path_cat(exepath, servicename + "-app" + ".py");
87
88
        string pythonpath = string("PYTHONPATH=") +
89
            path_cat(g_datadir, "cdplugins") + ":" +
90
            path_cat(g_datadir, "cdplugins/pycommon") + ":" +
91
            path_cat(g_datadir, "cdplugins/" + servicename);
92
        if (!cmd->startCmd(exepath, {/*args*/},
93
                          /* env */ {pythonpath})) {
94
            LOGERR("ServiceCreds::maybeStartCmd: startCmd failed\n");
95
            return false;
96
        }
97
        LOGDEB("ServiceCreds: " << servicename << " cmd started\n");
98
        return true;
99
    }
100
101
    string login() {
102
        LOGDEB("ServiceCreds: " << servicename << " login\n");
103
        if (!token.empty()) {
104
            return token;
105
        }
106
        if (!maybeStartCmd()) {
107
            return string();
108
        }
109
        unordered_map<string,string> res;
110
        if (!cmd->callproc("login", {{"user", user},
111
                    {"password", password}}, res)) {
112
            LOGERR("ServiceCreds::login: slave failure. Service " <<
113
                   servicename << " user " << user << endl);
114
            return string();
115
        }
116
        auto it = res.find("appid");
117
        if (it == res.end()) {
118
            LOGERR("ServiceCreds::login: no appid. Service " <<
119
                   servicename << " user " << user << endl);
120
            return string();
121
        }
122
        appid = it->second;
123
        data = appid;
124
        it = res.find("token");
125
        if (it == res.end()) {
126
            LOGERR("ServiceCreds::login: no token. Service " <<
127
                   servicename << " user " << user << endl);
128
            return string();
129
        }
130
        return token = it->second;
131
    }
132
133
    void logout() {
134
        appid.clear();
135
        token.clear();
136
    }
137
138
    string str() {
139
        string s;
140
        s += "Service: " + servicename + " User: " + user +
141
            " Pass: " + password + " Appid " + appid + " Token " + token +
142
            " EPass: " + encryptedpass + " Enabled: " +
143
            SoapHelp::i2s(enabled) + " Status: " + status + " Data: " + data;
144
        return s;
145
    }
146
147
    // Internal name, like "qobuz"
148
    string servicename;
149
    string user;
150
    string password;
151
    string encryptedpass;
152
    string appid;
153
    string token;
154
    bool enabled{true};
155
    string status;
156
    // For qobuz, data contains the "app id" 854233864 for ohplayer.
157
    // XBMC is 285473059
158
    string data;
159
    CmdTalk *cmd{0};
160
};
161
162
class OHCredentials::Internal {
163
public:
164
    
165
    Internal(const string& cd) {
166
        string cachedir = path_cat(cd, "ohcreds");
167
        if (!path_makepath(cachedir, 0700)) {
168
            LOGERR("OHCredentials: can't create cache dir " << cachedir <<endl);
169
            return;
170
        }
171
        keyfile = path_cat(cachedir, "credkey.pem");
172
        cmd.putenv("RANDFILE", path_cat(cachedir, "randfile"));
173
174
        if (!path_exists(keyfile)) {
175
            vector<string> acmd{"openssl", "genrsa", "-out", keyfile, "4096"};
176
            int status = cmd.doexec1(acmd);
177
            if (status != 0) {
178
                LOGERR("OHCredentials: could not create key\n");
179
                return;
180
            }
181
        }
182
183
        vector<string> acmd{"openssl", "pkey", "-in", keyfile, "-pubout"};
184
        if (!cmd.backtick(acmd, pubkey)) {
185
            LOGERR("OHCredentials: could not read public key\n");
186
            return;
187
        }
188
189
        LOGDEB("OHCredentials: my public key:\n" << pubkey << endl);
190
    }
191
192
    bool decrypt(const string& in, string& out) {
193
        vector<string> acmd{"openssl", "pkeyutl", "-inkey",
194
                keyfile, "-pkeyopt", "rsa_padding_mode:oaep", "-decrypt"};
195
        int status = cmd.doexec1(acmd, &in, &out);
196
        if (status) {
197
            LOGERR("OHCredentials: decrypt failed\n");
198
            return false;
199
        }
200
        LOGDEB("decrypt: [" << out << "]\n");
201
        return true;
202
    }
203
204
    bool setEnabled(const string& id, bool enabled) {
205
        auto it = creds.find(id);
206
        if (it == creds.end()) {
207
            return false;
208
        }
209
        it->second.enabled = enabled;
210
        return true;
211
    }
212
    
213
    ExecCmd cmd;
214
    string keyfile;
215
    string pubkey;
216
    int seq{1};
217
    map<string, ServiceCreds> creds;
218
};
219
220
221
OHCredentials::OHCredentials(UpMpd *dev, const string& cachedir)
222
    : OHService(sTpCredentials, sIdCredentials, dev), m(new Internal(cachedir))
223
{
224
    dev->addActionMapping(
225
        this, "Set",
226
        bind(&OHCredentials::actSet, this, _1, _2));
227
    dev->addActionMapping(
228
        this, "Clear",
229
        bind(&OHCredentials::actClear, this, _1, _2));
230
    dev->addActionMapping(
231
        this, "SetEnabled",
232
        bind(&OHCredentials::actSetEnabled, this, _1, _2));
233
    dev->addActionMapping(
234
        this, "Get",
235
        bind(&OHCredentials::actGet, this, _1, _2));
236
    dev->addActionMapping(
237
        this, "Login",
238
        bind(&OHCredentials::actLogin, this, _1, _2));
239
    dev->addActionMapping(
240
        this, "ReLogin",
241
        bind(&OHCredentials::actReLogin, this, _1, _2));
242
    dev->addActionMapping(
243
        this, "GetIds",
244
        bind(&OHCredentials::actGetIds, this, _1, _2));
245
    dev->addActionMapping(
246
        this, "GetPublicKey",
247
        bind(&OHCredentials::actGetPublicKey, this, _1, _2));
248
    dev->addActionMapping(
249
        this, "GetSequenceNumber",
250
        bind(&OHCredentials::actGetSequenceNumber, this, _1, _2));
251
}
252
253
OHCredentials::~OHCredentials()
254
{
255
    delete m;
256
}
257
258
bool OHCredentials::makestate(unordered_map<string, string> &st)
259
{
260
    st.clear();
261
    if (nullptr == m) {
262
        return false;
263
    }
264
    st["Ids"] = idstring;
265
    st["PublicKey"] = m->pubkey;
266
    st["SequenceNumber"] = SoapHelp::i2s(m->seq);
267
    return true;
268
}
269
270
int OHCredentials::actSet(const SoapIncoming& sc, SoapOutgoing& data)
271
{
272
    bool ok = false;
273
    std::string in_Id;
274
    ok = sc.get("Id", &in_Id);
275
    if (!ok) {
276
        LOGERR("OHCredentials::actSet: no Id in params\n");
277
        return UPNP_E_INVALID_PARAM;
278
    }
279
    std::string in_UserName;
280
    ok = sc.get("UserName", &in_UserName);
281
    if (!ok) {
282
        LOGERR("OHCredentials::actSet: no UserName in params\n");
283
        return UPNP_E_INVALID_PARAM;
284
    }
285
    string in_Password;
286
    ok = sc.get("Password", &in_Password);
287
    if (!ok) {
288
        LOGERR("OHCredentials::actSet: no Password in params\n");
289
        return UPNP_E_INVALID_PARAM;
290
    }
291
292
    LOGDEB("OHCredentials::actSet: " << " Id " << in_Id << " UserName " <<
293
           in_UserName << " Password " << in_Password << endl);
294
295
    const auto it1 = idmap.find(in_Id);
296
    if (it1 == idmap.end()) {
297
        LOGERR("OHCredentials::actSet: bad service id [" << in_Id <<"]\n");
298
        return UPNP_E_INVALID_PARAM;
299
    }
300
    string servicename = it1->second;
301
    string cpass = base64_decode(in_Password);
302
    string plainpass;
303
    if (!m->decrypt(cpass, plainpass)) {
304
        LOGERR("OHCredentials::actSet: could not decrypt\n");
305
        return UPNP_E_INVALID_PARAM;
306
    }
307
    auto it = m->creds.find(in_Id);
308
    if (it == m->creds.end() || it->second.user != in_UserName ||
309
        it->second.password != plainpass ||
310
        it->second.encryptedpass != in_Password) {
311
        m->creds[in_Id] = ServiceCreds(servicename, in_UserName, plainpass,
312
                                       in_Password);
313
    }
314
    m->seq++;
315
316
    if (m->setEnabled(in_Id, true)) {
317
        return UPNP_E_SUCCESS;
318
    } else {
319
        return UPNP_E_INVALID_PARAM;
320
    }
321
}
322
323
int OHCredentials::actLogin(const SoapIncoming& sc, SoapOutgoing& data)
324
{
325
    bool ok = false;
326
    std::string in_Id;
327
    ok = sc.get("Id", &in_Id);
328
    if (!ok) {
329
        LOGERR("OHCredentials::actLogin: no Id in params\n");
330
        return UPNP_E_INVALID_PARAM;
331
    }
332
333
    LOGDEB("OHCredentials::actLogin: " << " Id " << in_Id << endl);
334
    auto it = m->creds.find(in_Id);
335
    if (it == m->creds.end()) {
336
        LOGERR("OHCredentials::Login: Id " << in_Id << " not found\n");
337
        return UPNP_E_INVALID_PARAM;
338
    }
339
    string token = it->second.login();
340
    LOGDEB("OHCredentials::Login: got token " << token << endl);
341
    data.addarg("Token", token);
342
    m->seq++;
343
    return UPNP_E_SUCCESS;
344
}
345
346
int OHCredentials::actReLogin(const SoapIncoming& sc, SoapOutgoing& data)
347
{
348
    bool ok = false;
349
    std::string in_Id;
350
    ok = sc.get("Id", &in_Id);
351
    if (!ok) {
352
        LOGERR("OHCredentials::actReLogin: no Id in params\n");
353
        return UPNP_E_INVALID_PARAM;
354
    }
355
    std::string in_CurrentToken;
356
    ok = sc.get("CurrentToken", &in_CurrentToken);
357
    if (!ok) {
358
        LOGERR("OHCredentials::actReLogin: no CurrentToken in params\n");
359
        return UPNP_E_INVALID_PARAM;
360
    }
361
362
    LOGDEB("OHCredentials::actReLogin: " << " Id " << in_Id << " CurrentToken "
363
           << in_CurrentToken << endl);
364
365
    auto it = m->creds.find(in_Id);
366
    if (it == m->creds.end()) {
367
        LOGERR("OHCredentials::Login: Id " << in_Id << " not found\n");
368
        return UPNP_E_INVALID_PARAM;
369
    }
370
    it->second.logout();
371
    string token = it->second.login();
372
    data.addarg("NewToken", token);
373
    m->seq++;
374
    return UPNP_E_SUCCESS;
375
}
376
377
int OHCredentials::actClear(const SoapIncoming& sc, SoapOutgoing& data)
378
{
379
    bool ok = false;
380
    std::string in_Id;
381
    ok = sc.get("Id", &in_Id);
382
    if (!ok) {
383
        LOGERR("OHCredentials::actClear: no Id in params\n");
384
        return UPNP_E_INVALID_PARAM;
385
    }
386
387
    LOGDEB("OHCredentials::actClear: " << " Id " << in_Id << endl);
388
    if (idmap.find(in_Id) == idmap.end()) {
389
        LOGERR("OHCredentials::actClear: bad service id [" << in_Id <<"]\n");
390
        return UPNP_E_INVALID_PARAM;
391
    }
392
    auto it = m->creds.find(in_Id);
393
    if (it != m->creds.end()) {
394
        m->creds.erase(it);
395
        m->seq++;
396
    }
397
    return UPNP_E_SUCCESS;
398
}
399
400
int OHCredentials::actSetEnabled(const SoapIncoming& sc, SoapOutgoing& data)
401
{
402
    bool ok = false;
403
    std::string in_Id;
404
    ok = sc.get("Id", &in_Id);
405
    if (!ok) {
406
        LOGERR("OHCredentials::actSetEnabled: no Id in params\n");
407
        return UPNP_E_INVALID_PARAM;
408
    }
409
    bool in_Enabled;
410
    ok = sc.get("Enabled", &in_Enabled);
411
    if (!ok) {
412
        LOGERR("OHCredentials::actSetEnabled: no Enabled in params\n");
413
        return UPNP_E_INVALID_PARAM;
414
    }
415
416
    LOGDEB("OHCredentials::actSetEnabled: " << " Id " << in_Id << " Enabled " <<
417
           in_Enabled << endl);
418
    if (m->setEnabled(in_Id, in_Enabled)) {
419
        m->seq++;
420
        return UPNP_E_SUCCESS;
421
    } else {
422
        return UPNP_E_INVALID_PARAM;
423
    }
424
}
425
426
int OHCredentials::actGet(const SoapIncoming& sc, SoapOutgoing& data)
427
{
428
    bool ok = false;
429
    std::string in_Id;
430
    ok = sc.get("Id", &in_Id);
431
    if (!ok) {
432
        LOGERR("OHCredentials::actGet: no Id in params\n");
433
        return UPNP_E_INVALID_PARAM;
434
    }
435
436
    LOGDEB("OHCredentials::actGet: " << " Id " << in_Id << endl);
437
438
    auto it = m->creds.find(in_Id);
439
    ServiceCreds emptycreds;
440
    ServiceCreds *credsp(&emptycreds);
441
    if (it != m->creds.end()) {
442
        credsp = &(it->second);
443
    } else {
444
        LOGDEB("OHCredentials::actGet: nothing found for " << in_Id << endl);
445
    }
446
    LOGDEB("OHCredentials::actGet: data for " << in_Id << " " <<
447
           credsp->str() << endl);
448
    data.addarg("UserName", credsp->user);
449
    // Encrypted password !
450
    data.addarg("Password", credsp->encryptedpass);
451
    // In theory enabled is set in response to setEnabled() or
452
    // set(). In practise, if it is not set, we don't get to the qobuz
453
    // settings screen in kazoo.
454
    data.addarg("Enabled", credsp->enabled ? "1" : "1");
455
    data.addarg("Status", credsp->status);
456
    data.addarg("Data", credsp->data);
457
    return UPNP_E_SUCCESS;
458
}
459
460
int OHCredentials::actGetIds(const SoapIncoming& sc, SoapOutgoing& data)
461
{
462
    LOGDEB("OHCredentials::actGetIds: " << endl);
463
    data.addarg("Ids", idstring);
464
    return UPNP_E_SUCCESS;
465
}
466
467
int OHCredentials::actGetPublicKey(const SoapIncoming& sc, SoapOutgoing& data)
468
{
469
    LOGDEB("OHCredentials::actGetPublicKey: " << endl);
470
    data.addarg("PublicKey", m->pubkey);
471
    return m->pubkey.empty() ? UPNP_E_INTERNAL_ERROR : UPNP_E_SUCCESS;
472
}
473
474
int OHCredentials::actGetSequenceNumber(const SoapIncoming& sc,
475
                                        SoapOutgoing& data)
476
{
477
    LOGDEB("OHCredentials::actGetSequenceNumber: " << endl);
478
    data.addarg("SequenceNumber", SoapHelp::i2s(m->seq));
479
    return UPNP_E_SUCCESS;
480
}