]> git.basschouten.com Git - openhab-addons.git/blob
ba656ec64166f992788a66884d7e5203be2e50e6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.linky.internal.api;
14
15 import java.net.CookieStore;
16 import java.net.HttpCookie;
17 import java.net.URI;
18 import java.time.LocalDate;
19 import java.time.format.DateTimeFormatter;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeoutException;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.client.util.FormContentProvider;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpHeader;
31 import org.eclipse.jetty.util.Fields;
32 import org.jsoup.Jsoup;
33 import org.jsoup.nodes.Document;
34 import org.jsoup.nodes.Element;
35 import org.openhab.binding.linky.internal.LinkyConfiguration;
36 import org.openhab.binding.linky.internal.LinkyException;
37 import org.openhab.binding.linky.internal.dto.AuthData;
38 import org.openhab.binding.linky.internal.dto.AuthResult;
39 import org.openhab.binding.linky.internal.dto.ConsumptionReport;
40 import org.openhab.binding.linky.internal.dto.ConsumptionReport.Consumption;
41 import org.openhab.binding.linky.internal.dto.PrmInfo;
42 import org.openhab.binding.linky.internal.dto.UserInfo;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import com.google.gson.Gson;
47
48 /**
49  * {@link EnedisHttpApi} wraps the Enedis Webservice.
50  *
51  * @author GaĆ«l L'hopital - Initial contribution
52  */
53 @NonNullByDefault
54 public class EnedisHttpApi {
55     private static final DateTimeFormatter API_DATE_FORMAT = DateTimeFormatter.ofPattern("dd-MM-yyyy");
56     private static final String URL_APPS_LINCS = "https://apps.lincs.enedis.fr";
57     private static final String URL_MON_COMPTE = "https://mon-compte.enedis.fr";
58     private static final String URL_ENEDIS_AUTHENTICATE = URL_APPS_LINCS
59             + "/authenticate?target=https://mon-compte-particulier.enedis.fr/suivi-de-mesure/";
60     private static final String URL_COOKIE = "https://mon-compte-particulier.enedis.fr";
61
62     private final Logger logger = LoggerFactory.getLogger(EnedisHttpApi.class);
63     private final Gson gson;
64     private final HttpClient httpClient;
65     private final LinkyConfiguration config;
66     private boolean connected = false;
67
68     public EnedisHttpApi(LinkyConfiguration config, Gson gson, HttpClient httpClient) {
69         this.gson = gson;
70         this.httpClient = httpClient;
71         this.config = config;
72     }
73
74     public void initialize() throws LinkyException {
75         httpClient.getSslContextFactory().setExcludeCipherSuites(new String[0]);
76         httpClient.setFollowRedirects(false);
77         try {
78             httpClient.start();
79         } catch (Exception e) {
80             throw new LinkyException("Unable to start Jetty HttpClient", e);
81         }
82         connect();
83     }
84
85     private void connect() throws LinkyException {
86         addCookie(LinkyConfiguration.INTERNAL_AUTH_ID, config.internalAuthId);
87
88         logger.debug("Starting login process for user : {}", config.username);
89
90         try {
91             logger.debug("Step 1 : getting authentification");
92             String data = getData(URL_ENEDIS_AUTHENTICATE);
93
94             logger.debug("Reception request SAML");
95             Document htmlDocument = Jsoup.parse(data);
96             Element el = htmlDocument.select("form").first();
97             Element samlInput = el.select("input[name=SAMLRequest]").first();
98
99             logger.debug("Step 2 : send SSO SAMLRequest");
100             ContentResponse result = httpClient.POST(el.attr("action"))
101                     .content(getFormContent("SAMLRequest", samlInput.attr("value"))).send();
102             if (result.getStatus() != 302) {
103                 throw new LinkyException("Connection failed step 2");
104             }
105
106             logger.debug("Get the location and the ReqID");
107             Pattern p = Pattern.compile("ReqID%(.*?)%26");
108             Matcher m = p.matcher(getLocation(result));
109             if (!m.find()) {
110                 throw new LinkyException("Unable to locate ReqId in header");
111             }
112
113             String reqId = m.group(1);
114             String url = URL_MON_COMPTE
115                     + "/auth/json/authenticate?realm=/enedis&forward=true&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
116                     + reqId
117                     + "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
118
119             logger.debug(
120                     "Step 3 : auth1 - retrieve the template, thanks to cookie internalAuthId, user is already set");
121             result = httpClient.POST(url).send();
122             if (result.getStatus() != 200) {
123                 throw new LinkyException("Connection failed step 3 - auth1 : " + result.getContentAsString());
124             }
125
126             AuthData authData = gson.fromJson(result.getContentAsString(), AuthData.class);
127             if (authData.callbacks.size() < 2 || authData.callbacks.get(0).input.size() == 0
128                     || authData.callbacks.get(1).input.size() == 0
129                     || !config.username.contentEquals(authData.callbacks.get(0).input.get(0).valueAsString())) {
130                 throw new LinkyException("Authentication error, the authentication_cookie is probably wrong");
131             }
132
133             authData.callbacks.get(1).input.get(0).value = config.password;
134             url = "https://mon-compte.enedis.fr/auth/json/authenticate?realm=/enedis&spEntityID=SP-ODW-PROD&goto=/auth/SSOPOST/metaAlias/enedis/providerIDP?ReqID%"
135                     + reqId
136                     + "%26index%3Dnull%26acsURL%3Dhttps://apps.lincs.enedis.fr/saml/SSO%26spEntityID%3DSP-ODW-PROD%26binding%3Durn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&AMAuthCookie=";
137
138             logger.debug("Step 3 : auth2 - send the auth data");
139             result = httpClient.POST(url).header(HttpHeader.CONTENT_TYPE, "application/json")
140                     .content(new StringContentProvider(gson.toJson(authData))).send();
141             if (result.getStatus() != 200) {
142                 throw new LinkyException("Connection failed step 3 - auth2 : " + result.getContentAsString());
143             }
144
145             AuthResult authResult = gson.fromJson(result.getContentAsString(), AuthResult.class);
146             logger.debug("Add the tokenId cookie");
147             addCookie("enedisExt", authResult.tokenId);
148
149             logger.debug("Step 4 : retrieve the SAMLresponse");
150             data = getData(URL_MON_COMPTE + "/" + authResult.successUrl);
151             htmlDocument = Jsoup.parse(data);
152             el = htmlDocument.select("form").first();
153             samlInput = el.select("input[name=SAMLResponse]").first();
154
155             logger.debug("Step 5 : post the SAMLresponse to finish the authentication");
156             result = httpClient.POST(el.attr("action")).content(getFormContent("SAMLResponse", samlInput.attr("value")))
157                     .send();
158             if (result.getStatus() != 302) {
159                 throw new LinkyException("Connection failed step 5");
160             }
161             connected = true;
162         } catch (InterruptedException | TimeoutException | ExecutionException e) {
163             throw new LinkyException("Error opening connection with Enedis webservice", e);
164         }
165     }
166
167     public String getLocation(ContentResponse response) {
168         return response.getHeaders().get(HttpHeader.LOCATION);
169     }
170
171     public void disconnect() throws LinkyException {
172         if (connected) {
173             try { // Three times in a row to get disconnected
174                 String location = getLocation(httpClient.GET(URL_APPS_LINCS + "/logout"));
175                 location = getLocation(httpClient.GET(location));
176                 location = getLocation(httpClient.GET(location));
177                 CookieStore cookieStore = httpClient.getCookieStore();
178                 cookieStore.removeAll();
179                 connected = false;
180             } catch (InterruptedException | ExecutionException | TimeoutException e) {
181                 throw new LinkyException("Error while disconnecting from Enedis webservice", e);
182             }
183         }
184     }
185
186     public void dispose() throws LinkyException {
187         try {
188             disconnect();
189             httpClient.stop();
190         } catch (Exception e) {
191             throw new LinkyException("Error stopping Jetty client", e);
192         }
193     }
194
195     private void addCookie(String key, String value) {
196         CookieStore cookieStore = httpClient.getCookieStore();
197         HttpCookie cookie = new HttpCookie(key, value);
198         cookie.setDomain(".enedis.fr");
199         cookie.setPath("/");
200         cookieStore.add(URI.create(URL_COOKIE), cookie);
201     }
202
203     private FormContentProvider getFormContent(String fieldName, String fieldValue) {
204         Fields fields = new Fields();
205         fields.put(fieldName, fieldValue);
206         return new FormContentProvider(fields);
207     }
208
209     private String getData(String url) throws LinkyException {
210         try {
211             ContentResponse result = httpClient.GET(url);
212             if (result.getStatus() != 200) {
213                 throw new LinkyException(String.format("Error requesting '%s' : %s", url, result.getContentAsString()));
214             }
215             return result.getContentAsString();
216         } catch (InterruptedException | ExecutionException | TimeoutException e) {
217             throw new LinkyException(String.format("Error getting url : '%s'", url), e);
218         }
219     }
220
221     public PrmInfo getPrmInfo() throws LinkyException {
222         final String prm_info_url = URL_APPS_LINCS + "/mes-mesures/api/private/v1/personnes/null/prms";
223         String data = getData(prm_info_url);
224         PrmInfo[] prms = gson.fromJson(data, PrmInfo[].class);
225         return prms[0];
226     }
227
228     public UserInfo getUserInfo() throws LinkyException {
229         final String user_info_url = URL_APPS_LINCS + "/userinfos";
230         String data = getData(user_info_url);
231         return gson.fromJson(data, UserInfo.class);
232     }
233
234     private Consumption getMeasures(String userId, String prmId, LocalDate from, LocalDate to, String request)
235             throws LinkyException {
236         final String measure_url = URL_APPS_LINCS
237                 + "/mes-mesures/api/private/v1/personnes/%s/prms/%s/donnees-%s?dateDebut=%s&dateFin=%s&mesuretypecode=CONS";
238         String url = String.format(measure_url, userId, prmId, request, from.format(API_DATE_FORMAT),
239                 to.format(API_DATE_FORMAT));
240         String data = getData(url);
241         ConsumptionReport report = gson.fromJson(data, ConsumptionReport.class);
242         return report.firstLevel.consumptions;
243     }
244
245     public Consumption getEnergyData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
246         return getMeasures(userId, prmId, from, to, "energie");
247     }
248
249     public Consumption getPowerData(String userId, String prmId, LocalDate from, LocalDate to) throws LinkyException {
250         return getMeasures(userId, prmId, from, to, "pmax");
251     }
252 }