/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.fritzaha.internal.hardware; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpExchange; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.fritzaha.internal.hardware.callbacks.FritzahaCallback; import org.openhab.core.events.EventPublisher; import org.openhab.core.types.State; import org.openhab.io.net.http.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class handles requests to a Fritz!OS web interface for interfacing with * AVM home automation devices. It manages authentication and wraps commands. * * @author Christian Brauers * @since 1.3.0 */ public class FritzahaWebInterface { /** * Host name of web interface */ protected String host; /** * Port under which web interface can be accessed */ protected int port; /** * Protocol to use to access web interface (http or https) */ protected String protocol; /** * Username to use for the web interface (if configured) */ protected String username; /** * Password to the web interface */ protected String password; /** * Current session ID */ protected String sid; /** * HTTP client for asynchronous calls */ protected HttpClient asyncclient; /** * Timeout for synchronous HTTP requests to web interface in milliseconds */ protected int timeout; /** * Timeout for asynchronous HTTP requests to web interface in milliseconds */ protected int asynctimeout; /** * Maximum number of simultaneous asynchronous connections */ protected int asyncmaxconns = 20; /** * Event publisher used by binding */ protected EventPublisher eventPublisher; static final Logger logger = LoggerFactory.getLogger(FritzahaWebInterface.class); // Uses RegEx to handle bad FritzBox XML /** * RegEx Pattern to grab the session ID from a login XML response */ protected static final Pattern SID_PATTERN = Pattern.compile("<SID>([a-fA-F0-9]*)</SID>"); /** * RegEx Pattern to grab the challenge from a login XML response */ protected static final Pattern CHALLENGE_PATTERN = Pattern.compile("<Challenge>(\\w*)</Challenge>"); /** * RegEx Pattern to grab the access privilege for home automation functions * from a login XML response */ protected static final Pattern ACCESS_PATTERN = Pattern .compile("<Name>HomeAuto</Name>\\s*?<Access>([0-9])</Access>"); /** * This method authenticates with the Fritz!OS Web Interface and updates the * session ID accordingly * * @return New session ID */ public String authenticate() { String loginXml = HttpUtil.executeUrl("GET", getURL("login_sid.lua", addSID("")), 10 * timeout); if (loginXml == null) { logger.error("FritzBox does not respond"); return null; } Matcher sidmatch = SID_PATTERN.matcher(loginXml); if (!sidmatch.find()) { logger.error("FritzBox does not respond with SID"); logger.debug("Output:\n" + loginXml); return null; } sid = sidmatch.group(1); Matcher accmatch = ACCESS_PATTERN.matcher(loginXml); if (accmatch.find()) { if (accmatch.group(1) == "2") { logger.info("Resuming FritzBox connection with SID " + sid); } return sid; } Matcher challengematch = CHALLENGE_PATTERN.matcher(loginXml); if (!challengematch.find()) { logger.error("FritzBox does not respond with challenge for authentication"); return null; } String challenge = challengematch.group(1); String response = createResponse(challenge); loginXml = HttpUtil.executeUrl("GET", getURL("login_sid.lua", (!username.equals("") ? ("username=" + username + "&") : "") + "response=" + response), timeout); if (loginXml == null) { logger.error("FritzBox does not respond"); return null; } sidmatch = SID_PATTERN.matcher(loginXml); if (!sidmatch.find()) { logger.error("FritzBox does not respond with SID"); return null; } sid = sidmatch.group(1); accmatch = ACCESS_PATTERN.matcher(loginXml); if (accmatch.find()) { if (accmatch.group(1) == "2") { logger.info("Established FritzBox connection with SID " + sid); } return sid; } logger.error("User " + username + " has no access to FritzBox home automation functions"); return null; } /** * Checks the authentication status of the web interface * * @return */ public boolean isAuthenticated() { return !(sid == null); } /** * Creates the proper response to a given challenge based on the password * stored * * @param challenge * Challenge string as returned by the Fritz!OS login script * @return Response to the challenge */ protected String createResponse(String challenge) { String handshake = challenge.concat("-").concat(password); MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { logger.error("This version of Java does not support MD5 hashing"); return ""; } byte[] handshakeHash; try { handshakeHash = md5.digest(handshake.getBytes("UTF-16LE")); } catch (UnsupportedEncodingException e) { logger.error("This version of Java does not understand UTF-16LE encoding"); return ""; } String response = challenge.concat("-"); for (byte handshakeByte : handshakeHash) { response = response.concat(String.format("%02x", handshakeByte)); } return response; } /** * Constructor to set up interface * * @param host * Hostname/IP address of Fritzbox * @param port * Port to use for Fritzbox connection * @param protocol * Protocol to use (HTTP,HTTPS) * @param username * Username for login * @param password * Password for login * @param synctimeout * Timeout for synchronous http-connections * @param asynctimeout * Timeout for asynchronous http-connections */ public FritzahaWebInterface(String host, int port, String protocol, String username, String password, int synctimeout, int asynctimeout) { this.host = host; this.port = port; this.protocol = protocol; this.username = username; this.password = password; this.timeout = synctimeout; this.asynctimeout = asynctimeout; sid = null; asyncclient = new HttpClient(new SslContextFactory(true)); asyncclient.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); asyncclient.setMaxConnectionsPerAddress(asyncmaxconns); asyncclient.setTimeout(asynctimeout); try { asyncclient.start(); } catch (Exception e) { logger.error("Could not start HTTP Client for " + getURL("")); } authenticate(); logger.debug("Starting with SID " + sid); } /** * Constructs a URL from the stored information and a specified path * * @param path * Path to include in URL * @return URL */ public String getURL(String path) { return protocol + "://" + host + ((port != -1) ? (":" + port) : "") + "/" + path; } /** * Constructs a URL from the stored information, a specified path and a * specified argument string * * @param path * Path to include in URL * @param args * String of arguments, in standard HTTP format * (arg1=value1&arg2=value2&...) * @return URL */ public String getURL(String path, String args) { return getURL(path + "?" + args); } public String addSID(String args) { if (sid == null) { return args; } else { return ("".equals(args) ? ("sid=") : (args + "&sid=")) + sid; } } /** * Sends an HTTP GET request using the asynchronous client * * @param Path * Path of the requested resource * @param Args * Arguments for the request * @param Callback * Callback to handle the response with */ public HttpExchange asyncGet(String path, String args, FritzahaCallback callback) { if (!isAuthenticated()) { authenticate(); } HttpExchange getExchange = new FritzahaContentExchange(callback); getExchange.setMethod("GET"); getExchange.setURL(getURL(path, addSID(args))); try { asyncclient.send(getExchange); } catch (IOException e) { logger.error("An I/O error occurred while sending the GET request " + getURL(path, addSID(args))); return null; } logger.debug("GETting URL " + getURL(path, addSID(args))); return getExchange; } /** * Sends an HTTP POST request using the asynchronous client * * @param Path * Path of the requested resource * @param Args * Arguments for the request * @param Callback * Callback to handle the response with */ public HttpExchange asyncPost(String path, String args, FritzahaCallback callback) { if (!isAuthenticated()) { authenticate(); } HttpExchange postExchange = new FritzahaContentExchange(callback); postExchange.setMethod("POST"); postExchange.setURL(getURL(path)); try { postExchange.setRequestContent(new ByteArrayBuffer(addSID(args).getBytes("UTF-8"))); } catch (UnsupportedEncodingException e1) { logger.error("An encoding error occurred in the POST arguments"); return null; } postExchange.setRequestContentType("application/x-www-form-urlencoded;charset=utf-8"); try { asyncclient.send(postExchange); } catch (IOException e) { logger.error("An I/O error occurred while sending the POST request to " + getURL(path)); return null; } return postExchange; } public void setEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = eventPublisher; } public void unsetEventPublisher(EventPublisher eventPublisher) { this.eventPublisher = null; } public void postUpdate(String itemName, State newState) { if (eventPublisher != null) { logger.debug("Sending update to item " + itemName); eventPublisher.postUpdate(itemName, newState); } else { logger.error("No event publisher for " + host); } } }