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