/
FritzBoxController.java
225 lines (193 loc) · 7.36 KB
/
FritzBoxController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package com.germancoding.fritzled;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.w3c.dom.Document;
public class FritzBoxController {
private DocumentBuilder builder;
private CloseableHttpClient client;
private Charset charset;
private String scheme;
private String domain;
private String username;
private String password;
private String sid; // TODO: Periodic checks if SID still valid
public FritzBoxController(String domain, String username, String password) throws ParserConfigurationException, URISyntaxException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
builder = documentBuilderFactory.newDocumentBuilder();
client = HttpClients.createDefault();
charset = Charset.forName("UTF-16LE");
scheme = "http://";
if (domain.toLowerCase().startsWith("http")) {
URI uri = new URI(domain);
scheme = uri.getScheme() + "://";
domain = uri.getHost() + uri.getPath();
}
if (domain.endsWith("/")) {
domain = domain.substring(0, domain.length() - 1);
}
this.domain = domain;
this.username = username;
this.password = password;
this.sid = getNewSessionID();
if (this.sid.equalsIgnoreCase("0000000000000000")) {
System.out.println("Login failed. Username/password may be incorrect.");
}
}
public String getNewSessionID() {
String url = getLoginAddress();
HttpGet get = new HttpGet(url);
CloseableHttpResponse httpResponse = null;
try {
httpResponse = client.execute(get);
Document document = builder.parse(httpResponse.getEntity().getContent());
int timeout = Integer.parseInt(document.getElementsByTagName("BlockTime").item(0).getTextContent());
if (timeout > 0) {
System.out.println("Brute force protection active, waiting " + timeout + " seconds");
Thread.sleep(timeout * 1000l);
return getNewSessionID();
}
String sid = document.getElementsByTagName("SID").item(0).getTextContent();
if (sid.equalsIgnoreCase("0000000000000000")) { // Invalid SID; New login required (non-null if no password required)
String challenge = document.getElementsByTagName("Challenge").item(0).getTextContent();
String response = getResponse(challenge, password);
URIBuilder uriBuilder = new URIBuilder(url);
if (username != null)
uriBuilder.addParameter("username", username);
uriBuilder.addParameter("response", response);
HttpGet finalGet = new HttpGet(uriBuilder.build());
httpResponse.close();
httpResponse = client.execute(finalGet);
document = builder.parse(httpResponse.getEntity().getContent());
sid = document.getElementsByTagName("SID").item(0).getTextContent();
// System.out.println("new SID is " + sid);
}
return sid;
} catch (Throwable e) {
e.printStackTrace();
return null;
} finally {
if (httpResponse != null)
try {
httpResponse.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void setLEDStatus(boolean on) {
// System.out.println("LEDs to " + on);
String url = getDataAddress();
HttpPost post = new HttpPost(url);
// We first need to check if the device is dimmable and/or has dynamic light sensors and if so, get the current values
int dimValue = 2;
int envLight = 0;
ArrayList<NameValuePair> dimParams = new ArrayList<NameValuePair>();
dimParams.add(new BasicNameValuePair("sid", sid));
dimParams.add(new BasicNameValuePair("page", "led"));
CloseableHttpResponse response = null;
try {
post.setEntity(new UrlEncodedFormEntity(dimParams));
response = sendPost(post);
JSONTokener tokener = new JSONTokener(response.getEntity().getContent());
JSONObject responseObject = new JSONObject(tokener);
JSONObject parent = responseObject.getJSONObject("data").getJSONObject("ledSettings");
try {
dimValue = parent.getInt("dimValue");
} catch (JSONException e) {
// Might be unsupported
}
try {
envLight = parent.getInt("envLight");
} catch (JSONException e) {
// Might be unsupported
}
} catch (Exception e) {
// Ignore, this may be an older Fritz!OS device
;
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
;
}
}
}
ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("sid", sid));
params.add(new BasicNameValuePair("apply", ""));
params.add(new BasicNameValuePair("led_display", on ? "0" : "2"));
params.add(new BasicNameValuePair("oldpage", "/system/led_display.lua"));
params.add(new BasicNameValuePair("ledDisplay", on ? "0" : "2"));
params.add(new BasicNameValuePair("dimValue", Integer.toString(dimValue)));
params.add(new BasicNameValuePair("envLight", Integer.toString(envLight)));
params.add(new BasicNameValuePair("page", "led"));
try {
post.setEntity(new UrlEncodedFormEntity(params));
sendPost(post).close();
// System.out.println("POST send with sid " + sid);
} catch (ClientProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public CloseableHttpResponse sendPost(HttpPost post) throws ClientProtocolException, IOException {
return client.execute(post);
}
public String getSID() {
return sid;
}
private String getResponse(String challenge, String password) {
try {
String message = challenge + "-" + password;
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(message.getBytes(charset));
return challenge + "-" + bytesToHex(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
// Copied from https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java
private final static char[] hexArray = "0123456789abcdef".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public String getLoginAddress() {
return scheme + domain + "/login_sid.lua";
}
public String getDataAddress() {
return scheme + domain + "/data.lua";
}
public String getFirmwareCfgAddress() {
return scheme + domain + "/cgi-bin/firmwarecfg";
}
}