2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.magentatv.internal.network;
15 import static org.openhab.binding.magentatv.internal.MagentaTVBindingConstants.*;
16 import static org.openhab.binding.magentatv.internal.MagentaTVUtil.*;
18 import java.net.HttpCookie;
19 import java.net.URLEncoder;
20 import java.nio.charset.StandardCharsets;
21 import java.text.MessageFormat;
22 import java.util.ArrayList;
23 import java.util.Enumeration;
24 import java.util.List;
25 import java.util.Properties;
26 import java.util.UUID;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import javax.ws.rs.HttpMethod;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.StringContentProvider;
39 import org.eclipse.jetty.http.HttpField;
40 import org.eclipse.jetty.http.HttpFields;
41 import org.eclipse.jetty.http.HttpHeader;
42 import org.eclipse.jetty.http.HttpStatus;
43 import org.openhab.binding.magentatv.internal.MagentaTVException;
44 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponse;
45 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthAuthenticateResponseInstanceCreator;
46 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponse;
47 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OAuthTokenResponseInstanceCreator;
48 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentials;
49 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthCredentialsInstanceCreator;
50 import org.openhab.binding.magentatv.internal.MagentaTVGsonDTO.OauthKeyValue;
51 import org.openhab.binding.magentatv.internal.handler.MagentaTVControl;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
55 import com.google.gson.Gson;
56 import com.google.gson.GsonBuilder;
59 * The {@link MagentaTVOAuth} class implements the OAuth authentication, which
60 * is used to query the userID from the Telekom platform.
62 * @author Markus Michels - Initial contribution
64 * Deutsche Telekom uses an OAuth-based authentication to access the EPG portal. The
65 * communication between the MR and the remote app requires a pairing before the receiver could be
66 * controlled by sending keys etc. The so called userID is not directly derived from any local parameters
67 * (like terminalID as a has from the mac address), but will be returned as a result from the OAuth
68 * authentication. This will be performed in 3 steps
69 * 1. Get OAuth credentials -> Service URL, Scope, Secret, Client ID
70 * 2. Get OAth Token -> authentication token for step 3
71 * 3. Authenticate, which then provides the userID (beside other parameters)
75 public class MagentaTVOAuth {
76 private final Logger logger = LoggerFactory.getLogger(MagentaTVOAuth.class);
77 private HttpClient httpClient;
78 private final Gson gson;
79 private List<HttpCookie> cookies = new ArrayList<>();
81 public MagentaTVOAuth(HttpClient httpClient) {
82 this.httpClient = httpClient;
83 gson = new GsonBuilder().registerTypeAdapter(OauthCredentials.class, new OauthCredentialsInstanceCreator())
84 .registerTypeAdapter(OAuthTokenResponse.class, new OAuthTokenResponseInstanceCreator())
85 .registerTypeAdapter(OAuthAuthenticateResponse.class, new OAuthAuthenticateResponseInstanceCreator())
89 public String getUserId(String accountName, String accountPassword) throws MagentaTVException {
90 logger.debug("Authenticate with account {}", accountName);
91 if (accountName.isEmpty() || accountPassword.isEmpty()) {
92 throw new MagentaTVException("Credentials for OAuth missing, check thing config!");
96 Properties httpHeader = initHttpHeader();
98 String httpResponse = "";
100 // OAuth autentication results
101 String oAuthScope = "";
102 String oAuthService = "";
103 String epghttpsurl = "";
106 url = OAUTH_GET_CRED_URL + ":" + OAUTH_GET_CRED_PORT + OAUTH_GET_CRED_URI;
107 httpHeader.setProperty(HttpHeader.HOST.toString(), substringAfterLast(OAUTH_GET_CRED_URL, "/"));
108 httpResponse = httpRequest(HttpMethod.GET, url, httpHeader, "");
109 OauthCredentials cred = gson.fromJson(httpResponse, OauthCredentials.class);
110 epghttpsurl = getString(cred.epghttpsurl);
111 if (epghttpsurl.isEmpty()) {
112 throw new MagentaTVException("Unable to determine EPG url");
114 if (!epghttpsurl.contains("/EPG")) {
115 epghttpsurl = epghttpsurl + "/EPG";
117 logger.debug("OAuth: epghttpsurl = {}", epghttpsurl);
119 // get OAuth data from response
120 if (cred.sam3Para != null) {
121 for (OauthKeyValue si : cred.sam3Para) {
122 logger.trace("sam3Para.{} = {}", si.key, si.value);
123 if ("oAuthScope".equalsIgnoreCase(si.key)) {
124 oAuthScope = si.value;
125 } else if ("SAM3ServiceURL".equalsIgnoreCase(si.key)) {
126 oAuthService = si.value;
130 if (oAuthScope.isEmpty() || oAuthService.isEmpty()) {
131 throw new MagentaTVException("OAuth failed: Can't get Scope and Service: " + httpResponse);
134 // Get OAuth token (New flow based on WebTV)
136 String terminalId = UUID.randomUUID().toString();
137 String cnonce = MagentaTVControl.computeMD5(terminalId);
139 url = oAuthService + "/oauth2/tokens";
140 postData = MessageFormat.format(
141 "password={0}&scope={1}+offline_access&grant_type=password&username={2}&x_telekom.access_token.format=CompactToken&x_telekom.access_token.encoding=text%2Fbase64&client_id=10LIVESAM30000004901NGTVWEB0000000000000",
142 urlEncode(accountPassword), oAuthScope, urlEncode(accountName));
143 url = oAuthService + "/oauth2/tokens";
144 httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData);
145 OAuthTokenResponse resp = gson.fromJson(httpResponse, OAuthTokenResponse.class);
146 if (resp.accessToken.isEmpty()) {
147 String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} ({2})",
148 accountName, getString(resp.errorDescription), getString(resp.error));
149 logger.warn("{}", errorMessage);
150 throw new MagentaTVException(errorMessage);
152 logger.debug("OAuth: Access Token retrieved");
154 // General authentication
155 logger.debug("OAuth: Generating CSRF token");
156 url = "https://api.prod.sngtv.magentatv.de/EPG/JSON/Authenticate";
157 httpHeader = initHttpHeader();
158 httpHeader.setProperty(HttpHeader.HOST.toString(), "api.prod.sngtv.magentatv.de");
159 httpHeader.setProperty("Origin", "https://web.magentatv.de");
160 httpHeader.setProperty(HttpHeader.REFERER.toString(), "https://web.magentatv.de/");
161 postData = "{\"areaid\":\"1\",\"cnonce\":\"" + cnonce + "\",\"mac\":\"" + terminalId
162 + "\",\"preSharedKeyID\":\"NGTV000001\",\"subnetId\":\"4901\",\"templatename\":\"NGTV\",\"terminalid\":\""
164 + "\",\"terminaltype\":\"WEB-MTV\",\"terminalvendor\":\"WebTV\",\"timezone\":\"Europe/Berlin\",\"usergroup\":\"-1\",\"userType\":3,\"utcEnable\":1}";
165 httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData);
167 for (HttpCookie c : cookies) { // get CRSF Token
168 String value = c.getValue();
169 if (value.contains("CSRFSESSION")) {
170 csrf = substringBetween(value, "CSRFSESSION" + "=", ";");
173 if (csrf.isEmpty()) {
174 throw new MagentaTVException("OAuth: Unable to get CSRF token!");
177 // Final step: Retrieve userId
178 url = "https://api.prod.sngtv.magentatv.de/EPG/JSON/DTAuthenticate";
179 httpHeader = initHttpHeader();
180 httpHeader.setProperty(HttpHeader.HOST.toString(), "api.prod.sngtv.magentatv.de");
181 httpHeader.setProperty("Origin", "https://web.magentatv.de");
182 httpHeader.setProperty(HttpHeader.REFERER.toString(), "https://web.magentatv.de/");
183 httpHeader.setProperty("X_CSRFToken", csrf);
184 postData = "{\"areaid\":\"1\",\"cnonce\":\"" + cnonce + "\",\"mac\":\"" + terminalId + "\","
185 + "\"preSharedKeyID\":\"NGTV000001\",\"subnetId\":\"4901\",\"templatename\":\"NGTV\","
186 + "\"terminalid\":\"" + terminalId + "\",\"terminaltype\":\"WEB-MTV\",\"terminalvendor\":\"WebTV\","
187 + "\"timezone\":\"Europe/Berlin\",\"usergroup\":\"\",\"userType\":\"1\",\"utcEnable\":1,"
188 + "\"accessToken\":\"" + resp.accessToken
189 + "\",\"caDeviceInfo\":[{\"caDeviceId\":\"4ef4d933-9a43-41d3-9e3a-84979f22c9eb\","
190 + "\"caDeviceType\":8}],\"connectType\":1,\"osversion\":\"Mac OS 10.15.7\",\"softwareVersion\":\"1.33.4.3\","
191 + "\"terminalDetail\":[{\"key\":\"GUID\",\"value\":\"" + terminalId + "\"},"
192 + "{\"key\":\"HardwareSupplier\",\"value\":\"WEB-MTV\"},{\"key\":\"DeviceClass\",\"value\":\"TV\"},"
193 + "{\"key\":\"DeviceStorage\",\"value\":0},{\"key\":\"DeviceStorageSize\",\"value\":0}]}";
194 httpResponse = httpRequest(HttpMethod.POST, url, httpHeader, postData);
195 OAuthAuthenticateResponse authResp = gson.fromJson(httpResponse, OAuthAuthenticateResponse.class);
196 if (authResp.userID.isEmpty()) {
197 String errorMessage = MessageFormat.format("Unable to authenticate: accountName={0}, rc={1} {2}",
198 accountName, getString(authResp.retcode), getString(authResp.desc));
199 logger.warn("{}", errorMessage);
200 throw new MagentaTVException(errorMessage);
202 userId = getString(authResp.userID);
203 if (userId.isEmpty()) {
204 throw new MagentaTVException("No userID received!");
206 String hashedUserID = MagentaTVControl.computeMD5(userId).toUpperCase();
207 logger.trace("done, userID = {}", hashedUserID);
211 private String httpRequest(String method, String url, Properties headers, String data) throws MagentaTVException {
214 Request request = httpClient.newRequest(url).method(method).timeout(NETWORK_TIMEOUT_MS,
215 TimeUnit.MILLISECONDS);
216 for (Enumeration<?> e = headers.keys(); e.hasMoreElements();) {
217 String key = (String) e.nextElement();
218 String val = (String) headers.get(key);
219 request.header(key, val);
221 if (method.equals(HttpMethod.POST)) {
222 fillPostData(request, data);
224 if (!cookies.isEmpty()) {
226 String cookieValue = "";
227 for (HttpCookie c : cookies) {
228 cookieValue = cookieValue + substringBefore(c.getValue(), ";") + "; ";
230 request.header("Cookie", substringBeforeLast(cookieValue, ";"));
232 logger.debug("OAuth: HTTP Request\n\tHTTP {} {}\n\tData={}", method, url, data.isEmpty() ? "<none>" : data);
233 logger.trace("\n\tHeaders={}\tCookies={}", request.getHeaders(), request.getCookies());
235 ContentResponse contentResponse = request.send();
236 result = contentResponse.getContentAsString().replace("\t", "").replace("\r\n", "").trim();
237 int status = contentResponse.getStatus();
238 logger.debug("OAuth: HTTP Response\n\tStatus={} {}\n\tData={}", status, contentResponse.getReason(),
239 result.isEmpty() ? "<none>" : result);
240 logger.trace("\n\tHeaders={}", contentResponse.getHeaders());
242 // validate response, API errors are reported as Json
243 HttpFields responseHeaders = contentResponse.getHeaders();
244 for (HttpField f : responseHeaders) {
245 if ("Set-Cookie".equals(f.getName())) {
246 HttpCookie c = new HttpCookie(f.getName(), f.getValue());
251 if (status != HttpStatus.OK_200) {
252 String error = "HTTP reqaest failed for URL " + url + ", Code=" + contentResponse.getReason() + "("
254 throw new MagentaTVException(error);
256 } catch (ExecutionException | InterruptedException | TimeoutException e) {
257 String error = "HTTP reqaest failed for URL " + url;
258 logger.info("{}", error, e);
259 throw new MagentaTVException(e, error);
264 private Properties initHttpHeader() {
265 Properties httpHeader = new Properties();
266 httpHeader.setProperty(HttpHeader.ACCEPT.toString(), "*/*");
267 httpHeader.setProperty(HttpHeader.ACCEPT_LANGUAGE.toString(), "en-US,en;q=0.9,de;q=0.8");
268 httpHeader.setProperty(HttpHeader.CACHE_CONTROL.toString(), "no-cache");
272 private void fillPostData(Request request, String data) {
273 if (!data.isEmpty()) {
274 StringContentProvider postData;
275 if (request.getHeaders().contains(HttpHeader.CONTENT_TYPE)) {
276 String contentType = request.getHeaders().get(HttpHeader.CONTENT_TYPE);
277 postData = new StringContentProvider(contentType, data, StandardCharsets.UTF_8);
279 boolean json = data.startsWith("{");
280 postData = new StringContentProvider(json ? "application/json" : "application/x-www-form-urlencoded",
281 data, StandardCharsets.UTF_8);
283 request.content(postData);
284 request.header(HttpHeader.CONTENT_LENGTH, Long.toString(postData.getLength()));
288 private String getString(@Nullable String value) {
289 return value != null ? value : "";
292 private String urlEncode(String url) {
293 return URLEncoder.encode(url, StandardCharsets.UTF_8);