/* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobilyzer.measurements;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.IOException;
import java.io.InvalidClassException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.mobilyzer.Config;
import com.mobilyzer.MeasurementDesc;
import com.mobilyzer.MeasurementResult;
import com.mobilyzer.MeasurementTask;
import com.mobilyzer.MeasurementResult.TaskProgress;
import com.mobilyzer.exceptions.MeasurementError;
import com.mobilyzer.util.Logger;
import com.mobilyzer.util.MeasurementJsonConvertor;
import com.mobilyzer.util.PhoneUtils;
import org.xbill.DNS.*;
/**
* Measures the DNS lookup time
*/
public class DnsLookupTask extends MeasurementTask {
// Type name for internal use
public static final String TYPE = "dns_lookup";
// Human readable name for the task
public static final String DESCRIPTOR = "DNS lookup";
//Since it's very hard to calculate the data consumed by this task
// directly, we use a fixed value. This is on the high side.
public static final int AVG_DATA_USAGE_BYTE = 2000;
private long duration;
/**
* The description of DNS lookup measurement
*/
public static class DnsLookupDesc extends MeasurementDesc {
public String target;
public String server;
public String [] servers;
public boolean hasMultiServer = false;
public String qclass;
public String qtype;
public DnsLookupDesc(String key, Date startTime, Date endTime,
double intervalSec, long count, long priority,
int contextIntervalSec, Map<String, String> params) {
super(DnsLookupTask.TYPE, key, startTime, endTime, intervalSec, count,
priority, contextIntervalSec, params);
initializeParams(params);
if (this.target == null || this.target.length() == 0) {
throw new InvalidParameterException("LookupDnsTask cannot " +
"be created due to null " +
"target string");
}
}
/*
* @see com.google.wireless.speed.speedometer.MeasurementDesc#getType()
*/
@Override
public String getType() {
return DnsLookupTask.TYPE;
}
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
if (!params.containsKey("server")){
ArrayList <String> servers = new ArrayList<String>();
try {
Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
Method method = SystemProperties.getMethod("get", new Class[]{String.class});
for (String name : new String[] { "net.dns1", "net.dns2", "net.dns3", "net.dns4", }) {
String value = (String) method.invoke(null, name);
if (value != null && !"".equals(value) && !servers.contains(value))
servers.add(value);
}
} catch(java.lang.ClassNotFoundException ex){
Logger.d("dns testing: dns local resolver: unable to get local resolver");
} catch(java.lang.NoSuchMethodException ex) {
Logger.d("dns testing: dns local resolver: unable to get local resolver");
} catch(java.lang.IllegalAccessException ex) {
Logger.d("dns testing: dns local resolver: unable to get local resolver");
} catch(java.lang.reflect.InvocationTargetException ex) {
Logger.d("dns testing: dns local resolver: unable to get local resolver");
}
this.hasMultiServer = true;
this.servers = servers.toArray(new String [0]);
//this.server = servers.get(0);
}else{
this.server = params.get("server");
if (this.server.contains("|")) {
this.servers = this.server.split("\\|");
this.hasMultiServer = true;
}
}
this.target = params.get("target");
// make the lookup absolute if it isn't already
if (!this.target.endsWith(".")) {
this.target = this.target + ".";
}
/* we are extending the DNS measurement to allow setting
* arbitrary query classes and types, but we want to maintain
* backwards compatibility. Therefore, we are going to default
* to a standard IPv4 query, qclass IN and qtype A
*/
if (params.containsKey("qclass")) {
this.qclass = params.get("qclass");
} else {
this.qclass = "IN";
}
if (params.containsKey("qtype")) {
this.qtype = params.get("qtype");
} else {
this.qtype = "A";
}
}
protected DnsLookupDesc(Parcel in) {
super(in);
target = in.readString();
server = in.readString();
qclass = in.readString();
qtype = in.readString();
}
public static final Parcelable.Creator<DnsLookupDesc> CREATOR =
new Parcelable.Creator<DnsLookupDesc>() {
public DnsLookupDesc createFromParcel(Parcel in) {
return new DnsLookupDesc(in);
}
public DnsLookupDesc[] newArray(int size) {
return new DnsLookupDesc[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(target);
dest.writeString(server);
dest.writeString(qclass);
dest.writeString(qtype);
}
}
private class DNSWrapper {
public boolean isValid;
public String rawOutput;
public Message response;
public int qid;
public int id;
public long respTime;
public String server;
public DNSWrapper(boolean isValid, byte[] rawOutput, Message response,
int qid, int id, long respTime, String server) {
this.isValid = isValid;
this.rawOutput = Base64.encodeToString(rawOutput, Base64.DEFAULT);
// this.rawOutput = rawOutput.toString();
this.response = response;
this.qid = qid;
this.id = id;
this.respTime = respTime;
this.server = server;
}
}
public DnsLookupTask(MeasurementDesc desc) {
super(new DnsLookupDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority,
desc.contextIntervalSec, desc.parameters));
this.duration = Config.DEFAULT_DNS_TASK_DURATION;
}
protected DnsLookupTask(Parcel in) {
super(in);
duration = in.readLong();
}
public static final Parcelable.Creator<DnsLookupTask> CREATOR =
new Parcelable.Creator<DnsLookupTask>() {
public DnsLookupTask createFromParcel(Parcel in) {
return new DnsLookupTask(in);
}
public DnsLookupTask[] newArray(int size) {
return new DnsLookupTask[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(duration);
}
/**
* Returns a copy of the DnsLookupTask
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
DnsLookupDesc newDesc = new DnsLookupDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters);
return new DnsLookupTask(newDesc);
}
public ArrayList<DNSWrapper> measureDNS(String domain, String qtype, String qclass, String server) {
Record question = null;
try {
question = Record.newRecord(Name.fromString(domain),
Type.value(qtype),
DClass.value(qclass));
} catch (TextParseException e) {
Logger.d("dns testing: Error constructing packet");
}
Logger.d("dns testing: constructed question");
Message query = Message.newQuery(question);
// wait for at most 5 seconds for a response
//long endTime = System.currentTimeMillis() + 5;
Logger.d("dns testing: constructed query");
ArrayList<DNSWrapper> responses = sendMeasurement(query, server, false);
return responses;
}
private ArrayList<DNSWrapper> sendMeasurement(Message query, String server, boolean useTCP) {
// now that we have a message, put it on the wire and wait for
// responses
int qid = query.getHeader().getID();
byte[] output = query.toWire();
int udpSize = SimpleResolver.maxUDPSize(query);
// DnsLookupDesc desc = (DnsLookupDesc) this.measurementDesc;
long endTime = System.currentTimeMillis() + 60 * 5 * 1000;
/* the people who wrote the DNS code were not awesome and didn't have abstract methods,
* so the code doesn't let me use their superclass, client. Therefore, I'm doing the hacky
* solution and creating 2 different clients
*/
TCPClient tclient = null;
UDPClient uclient = null;
if (useTCP || (output.length > udpSize)) {
try {
tclient = new TCPClient(endTime);
SocketAddress addr = new InetSocketAddress(server, 53);
tclient.connect(addr);
useTCP = true;
} catch (IOException e) {
Logger.d("dns testing: Error creating client");
}
} else {
try {
uclient = new UDPClient(endTime);
uclient.bind(null);
SocketAddress addr = new InetSocketAddress(server, 53);
uclient.connect(addr);
} catch (IOException e) {
Logger.e("dns testing: Error creating client");
}
}
Logger.d("dns testing: initialized client");
boolean shouldSend = true;
long startTime = 0;
long respTime;
ArrayList<DNSWrapper> responses = new ArrayList<DNSWrapper>();
Logger.d("dns testing: about to start loop current time " + System.currentTimeMillis() + " end time: " + endTime);
while (System.currentTimeMillis() < endTime) {
byte[] in = {};
if (shouldSend) {
try {
if (useTCP) tclient.send(output);
else uclient.send(output);
startTime = System.currentTimeMillis();
shouldSend = false;
} catch (IOException e) {
Logger.e("dns testing: Error sending");
}
}
try {
if (useTCP) {
in = tclient.recv();
} else {
in = uclient.recv(udpSize);
}
} catch (IOException e) {
Logger.d("dns testing: Problem receiving packet due to " + e.getMessage());
}
Logger.d("dns testing: received");
respTime = System.currentTimeMillis() - startTime;
// if we didn't get anything back, then continue. this
// means we will break out if we are over time
if (in.length == 0) {
Logger.d("dns testing: empty response, breaking out");
break;
}
DNSWrapper wrap;
Message response;
// don't parse the message if it's too short
if (in.length < Header.LENGTH) {
wrap = new DNSWrapper(false, in, null, qid, -1, respTime, server);
responses.add(wrap);
Logger.d("dns testing: nothing to parse");
continue;
}
int id = ((in[0] & 0xFF) << 8) + (in[1] & 0xFF);
try {
response = SimpleResolver.parseMessage(in);
wrap = new DNSWrapper(true, in, response, qid, id, respTime, server);
responses.add(wrap);
Logger.d("dns testing: successfully parsed response");
} catch (WireParseException e) {
Logger.e("dns testing: Problem trying to parse dns packet");
wrap = new DNSWrapper(false, in, null, qid, -1, respTime, server);
responses.add(wrap);
continue;
}
// if the response was truncated, then requery over TCP
if (!useTCP && response.getHeader().getFlag(Flags.TC)) {
try {
uclient.cleanup();
tclient = new TCPClient(endTime);
SocketAddress addr = new InetSocketAddress(server, 53);
tclient.connect(addr);
useTCP = true;
shouldSend = true;
Logger.d("dns testing: requerying over tcp");
} catch (IOException e) {
Logger.e("dns testing: Problem trying to retry over TCP");
}
}
}
return responses;
}
@Override
public MeasurementResult[] call() throws MeasurementError {
ArrayList<DNSWrapper> responses = new ArrayList<DNSWrapper>();
DnsLookupDesc desc = (DnsLookupDesc) this.measurementDesc;
for (int i = 0; i < Config.DEFAULT_DNS_COUNT_PER_MEASUREMENT; i++) {
DnsLookupDesc taskDesc = (DnsLookupDesc) this.measurementDesc;
Logger.i("Running DNS Lookup for target " + taskDesc.target);
if (taskDesc.hasMultiServer) {
for (String server: taskDesc.servers) {
Logger.i("dns test starting to measure against server " + server);
ArrayList<DNSWrapper> resps = measureDNS(taskDesc.target, taskDesc.qtype, taskDesc.qclass, server);
Logger.i("dns test recieved " + resps.size() + " responses");
responses.addAll(resps);
Logger.i("dns test added resps to overall responses");
}
} else {
responses = measureDNS(taskDesc.target, taskDesc.qtype, taskDesc.qclass, taskDesc.server);
}
}
if ((responses == null) || (responses.size() == 0)) {
throw new MeasurementError("Problems conducting DNS measurement");
} else {
Logger.i("Successfully resolved target address");
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
ArrayList<MeasurementResult> results = new ArrayList<MeasurementResult>();
MeasurementResult result;
// for (DNSWrapper wrap : responses) {
result = new MeasurementResult(
phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(this.getKey()),
DnsLookupTask.TYPE,
System.currentTimeMillis() * 1000,
TaskProgress.COMPLETED, this.measurementDesc);
// now turn the result into an array of hashmaps with the data we care about
List<HashMap<String, Object>> data = extractResults(responses);
result.addResult("results", data);
result.addResult("target", desc.target);
result.addResult("qtype", desc.qtype);
result.addResult("qclass", desc.qclass);
Logger.i(MeasurementJsonConvertor.toJsonString(result));
results.add(result);
// }
// create the result array to return
MeasurementResult resultsFinal [] = new MeasurementResult[results.size()];
for (int i = 0; i < resultsFinal.length; i++) {
resultsFinal[i] = results.get(i);
}
return resultsFinal;
}
}
public List<HashMap<String, Object>> extractResults(ArrayList<DNSWrapper> responses) {
ArrayList<HashMap<String, Object>> data = new ArrayList<HashMap<String, Object>>();
for (DNSWrapper wrap : responses) {
Message resp = null;
if (wrap.isValid) {
resp = wrap.response;
}
HashMap<String, Object> item = new HashMap<String, Object>();
item.put("server", wrap.server);
item.put("qryId", wrap.qid);
item.put("respId", wrap.id);
item.put("payload", wrap.rawOutput);
item.put("respTime", wrap.respTime);
item.put("isValid", wrap.isValid);
item.put("rcode", Rcode.string(resp.header.getRcode()));
item.put("tc", resp.getHeader().getFlag(Flags.TC));
// process the question
Record[] questionRecs = resp.getSectionArray(0);
if (questionRecs.length == 0) {
item.put("domain", null);
item.put("qtype", null);
item.put("qclass", null);
} else {
Record rec = questionRecs[0];
item.put("domain", rec.name.toString());
item.put("qtype", Type.string(rec.type));
item.put("qclass", DClass.string(rec.dclass));
}
// now process the answers
List<HashMap<String, String>> answers = new ArrayList<HashMap<String, String>>();
questionRecs = resp.getSectionArray(1);
for (Record recd : questionRecs) {
HashMap<String, String> entry = new HashMap<String, String>();
entry.put("name", recd.name.toString());
entry.put("rtype", Type.string(recd.type));
entry.put("rdata", recd.rrToString());
answers.add(entry);
}
item.put("answers", answers.toArray());
data.add(item);
}
return data;
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return DnsLookupDesc.class;
}
@Override
public String getType() {
return DnsLookupTask.TYPE;
}
@Override
public String getDescriptor() {
return DESCRIPTOR;
}
@Override
public String toString() {
DnsLookupDesc desc = (DnsLookupDesc) measurementDesc;
return "[DNS Lookup]\n Target: " + desc.target + "\n Interval (sec): "
+ desc.intervalSec + "\n Next run: " + desc.startTime;
}
@Override
public boolean stop() {
//There is nothing we need to do to stop the DNS measurement
return false;
}
@Override
public long getDuration() {
return this.duration;
}
@Override
public void setDuration(long newDuration) {
if (newDuration < 0) {
this.duration = 0;
} else {
this.duration = newDuration;
}
}
/**
* Since it is hard to get the amount of data sent directly,
* use a fixed value. The data consumed is usually small, and the fixed
* value is a conservative estimate.
* <p/>
* TODO find a better way to get this value
*/
@Override
public long getDataConsumed() {
return AVG_DATA_USAGE_BYTE;
}
}