/*
* AndFHEM - Open Source Android application to control a FHEM home automation
* server.
*
* Copyright (c) 2011, Matthias Klass or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU GENERAL PUBLIC LICENSE, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GENERAL PUBLIC LICENSE
* for more details.
*
* You should have received a copy of the GNU GENERAL PUBLIC LICENSE
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package li.klass.fhem.fhem;
import android.content.Context;
import android.graphics.Bitmap;
import org.apache.commons.net.telnet.TelnetClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import li.klass.fhem.fhem.connection.FHEMServerSpec;
import li.klass.fhem.util.ApplicationProperties;
import li.klass.fhem.util.CloseableUtil;
import li.klass.fhem.util.StringUtil;
public class TelnetConnection extends FHEMConnection {
public static final String TAG = TelnetConnection.class.getName();
private static final String PASSWORD_PROMPT = "Password: ";
private static final Logger LOG = LoggerFactory.getLogger(TelnetConnection.class.getName());
public TelnetConnection(FHEMServerSpec fhemServerSpec, ApplicationProperties applicationProperties) {
super(fhemServerSpec, applicationProperties);
}
public RequestResult<String> executeCommand(String command, Context context) {
LOG.info("executeTask command {}", command);
final TelnetClient telnetClient = new TelnetClient();
telnetClient.setConnectTimeout(getConnectionTimeoutMilliSeconds(context));
BufferedOutputStream bufferedOutputStream = null;
PrintStream printStream = null;
String errorHost = serverSpec.getIp() + ":" + serverSpec.getPort();
try {
telnetClient.connect(serverSpec.getIp(), serverSpec.getPort());
OutputStream outputStream = telnetClient.getOutputStream();
InputStream inputStream = telnetClient.getInputStream();
bufferedOutputStream = new BufferedOutputStream(outputStream);
printStream = new PrintStream(outputStream);
waitForFilledStream(inputStream, 5000);
String passwordRead = readUntil(inputStream, PASSWORD_PROMPT);
if (passwordRead != null && passwordRead.contains(PASSWORD_PROMPT)) {
LOG.info("sending password");
if (!writeCommand(inputStream, printStream, serverSpec.getPassword())) {
return new RequestResult<>(RequestResultError.AUTHENTICATION_ERROR);
}
}
if (!writeCommand(inputStream, printStream, command + "\r\n")) {
return new RequestResult<>(RequestResultError.HOST_CONNECTION_ERROR);
}
// If we send an xmllist, we are done when finding the closing FHZINFO tag.
// If another command is used, the tag ending delimiter is obsolete, not found and
// therefore not used. We just read until the stream ends.
String result;
if (command.equals("xmllist")) {
result = readUntil(inputStream, "</FHZINFO>");
} else {
result = read(inputStream);
}
if (result == null) {
return new RequestResult<>(RequestResultError.INVALID_CONTENT);
}
telnetClient.disconnect();
int startPos = result.indexOf(", try help");
if (startPos != -1) {
result = result.substring(startPos + ", try help".length());
}
startPos = result.indexOf("<");
if (startPos != -1) {
result = result.substring(startPos);
}
result = result
.replaceAll("Bye...", "")
.replaceAll("fhem>", "");
result = new String(result.getBytes("UTF8"));
LOG.debug("result is {}", result);
return new RequestResult<>(result);
} catch (SocketTimeoutException e) {
LOG.error("timeout", e);
setErrorInErrorHolderFor(e, errorHost, command);
return new RequestResult<>(RequestResultError.CONNECTION_TIMEOUT);
} catch (UnsupportedEncodingException e) {
// this may never happen, as UTF8 is known ...
setErrorInErrorHolderFor(e, errorHost, command);
throw new IllegalStateException("unsupported encoding", e);
} catch (SocketException e) {
// We handle host connection errors directly after connecting to the server by waiting
// for some token for some seconds. Afterwards, the only possibility for an error
// is that the FHEM server ends the connection after receiving an invalid password.
LOG.error("SocketException", e);
setErrorInErrorHolderFor(e, errorHost, command);
return new RequestResult<>(RequestResultError.AUTHENTICATION_ERROR);
} catch (IOException e) {
LOG.error("IOException", e);
setErrorInErrorHolderFor(e, errorHost, command);
return new RequestResult<>(RequestResultError.HOST_CONNECTION_ERROR);
} finally {
CloseableUtil.close(printStream, bufferedOutputStream);
}
}
private String readUntil(InputStream inputStream, String... blockers) throws IOException {
StringBuilder buffer = new StringBuilder();
while (true) {
if (inputStream.available() == 0 && ! waitForFilledStream(inputStream, 5000)) {
LOG.error("read data, but did not find end token, read content was '{}'", buffer.toString());
return null;
}
char readChar = (char) inputStream.read();
buffer.append(readChar);
for (String blocker : blockers) {
if (StringUtil.endsWith(buffer, blocker)) return buffer.toString();
}
}
}
private boolean writeCommand(InputStream inputStream, PrintStream printStream, String command) throws IOException {
LOG.debug("sending command {}", command);
printStream.println(command);
printStream.flush();
return waitForFilledStream(inputStream, 5000);
}
private boolean waitForFilledStream(InputStream inputStream, int timeToWait) throws IOException {
int initialFill = inputStream.available();
long startTime = System.currentTimeMillis();
while (inputStream.available() == initialFill &&
(System.currentTimeMillis() - startTime) < timeToWait) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
LOG.debug("interrupted, ignoring", e);
}
}
return inputStream.available() > 0;
}
private String read(InputStream inputStream) throws IOException {
waitForFilledStream(inputStream, 3000);
StringBuilder buffer = new StringBuilder();
while (inputStream.available() > 0 || waitForFilledStream(inputStream, 100)) {
char readChar = (char) inputStream.read();
buffer.append(readChar);
}
return buffer.toString();
}
@Override
public RequestResult<Bitmap> requestBitmap(String relativePath) {
LOG.debug("get image from relative path '{}'", relativePath);
return new RequestResult<>(null, null);
}
}