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