stun/src/main/java/com/github/freeacs/stun/Kick.java
package com.github.freeacs.stun;
import com.github.freeacs.common.util.IPAddress;
import com.github.freeacs.dbi.Unit;
import com.github.freeacs.dbi.crypto.Crypto;
import com.github.freeacs.dbi.util.SystemParameters;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.auth.params.AuthPNames;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.params.CoreConnectionPNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Kick {
@Data
@AllArgsConstructor
public static class KickResponse {
private final boolean kicked;
private String message;
}
private static final Kick kickSingleton = new Kick();
public static KickResponse kick(Unit unit, Properties properties)
throws MalformedURLException, SQLException {
return kickSingleton.kickInternal(unit, properties);
}
private static final Logger log = LoggerFactory.getLogger("KickSingle");
private static final Random random = new Random();
public KickResponse kickInternal(Unit unit, Properties properties)
throws MalformedURLException {
CPEParameters cpeParams = new CPEParameters(getKeyroot(unit));
String udpCrUrl = unit.getParameterValue(cpeParams.UDP_CONNECTION_URL, false);
String crUrl = unit.getParameterValue(cpeParams.CONNECTION_URL, false);
String crUser = unit.getParameterValue(cpeParams.CONNECTION_USERNAME);
String crPass = unit.getParameterValue(cpeParams.CONNECTION_PASSWORD);
if (crPass == null) {
crPass = crUser;
}
String publicIP = unit.getParameterValue(SystemParameters.IP_ADDRESS);
String publicProtocol = unit.getParameterValue(SystemParameters.PROTOCOL);
Integer publicPort = getPublicPort(unit);
// default response
KickResponse kr =
new KickResponse(
false,
"Neither a public ConnectionRequestURL nor any UDPConnectionRequestAddress was found");
// TCP-kick (HTTP)
if (crUrl != null && !crUrl.trim().isEmpty() && checkIfPublicIP(crUrl, properties)) {
log.debug(unit.getId() + ": will try TCP kick on " + crUrl);
return kickUsingTCP(unit, crUrl, crPass, crUser);
}
// UDP-kick
if (!kr.isKicked() && udpCrUrl != null && !udpCrUrl.trim().isEmpty()) {
log.debug(unit.getId() + ": will try UDP kick on " + udpCrUrl);
return kickUsingUDP(unit, udpCrUrl, crPass, crUser);
}
// TCP-kick with port forwarding
if (properties.isExpectPortForwarding() && publicIP != null && crUrl != null) {
String newCrUrl = crUrl.replace(new URL(crUrl).getHost(), publicIP);
log.debug(
unit.getId()
+ ": will try TCP kick by expecting port forwarding on "
+ crUrl
+ " -> "
+ newCrUrl);
return kickUsingTCP(unit, newCrUrl, crPass, crUser);
}
// Dynamic TCP-kick
if (crUrl == null && publicIP != null && publicProtocol != null && publicPort != null) {
crUrl = String.format("%s://%s:%d", publicProtocol, publicIP, publicPort);
log.debug(unit.getId() + ": will try dynamic TCP kick on " + crUrl);
return kickUsingTCP(unit, crUrl, crPass, crUser);
}
return kr;
}
private Integer getPublicPort(Unit unit) {
try {
return Integer.parseInt(unit.getParameterValue(SystemParameters.PORT));
} catch (NumberFormatException ex) {
return null;
}
}
/**
* Check if IP is public only if its configured. If its not configured, this method returns true
* always.
*
* @param crUrl The ip to check
* @param properties props
* @return a boolean if configured to check if ip is public, otherwise always true
* @throws MalformedURLException if the ip is malformed
*/
boolean checkIfPublicIP(String crUrl, Properties properties) throws MalformedURLException {
return !properties.isCheckPublicIp() || IPAddress.isPublic(new URL(crUrl).getHost());
}
protected KickResponse kickUsingTCP(Unit unit, String crUrl, String crPass, String crUser)
throws MalformedURLException {
DefaultHttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(crUrl);
get.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 20000);
get.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 20000);
client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(0, true));
int statusCode;
if (crUser != null && crPass != null) {
get = authenticate(client, get, crUrl, crUser, crPass);
log.debug(
unit.getId()
+ " had a password and username, hence the kick will be executed with authentication (digest/basic)");
}
try {
HttpResponse response = client.execute(get);
statusCode = response.getStatusLine().getStatusCode();
} catch (ConnectTimeoutException ce) {
log.warn(unit.getId() + " did not respond, indicating a NAT problem or disconnected.");
return new KickResponse(
false,
"TCP/HTTP-kick to "
+ crUrl
+ " failed, probably due to NAT or other connection problems: "
+ ce.getMessage());
} catch (Throwable t) {
log.warn(unit.getId() + " did not respond, an error has occured: ", t);
return new KickResponse(
false,
"TCP/HTTP-kick to "
+ crUrl
+ " failed because of an unexpected error: "
+ t.getMessage());
}
if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NO_CONTENT) {
log.debug(
unit.getId() + " responded with HTTP " + statusCode + ", indicating a successful kick");
return new KickResponse(
true,
"TCP/HTTP-kick to "
+ crUrl
+ " got HTTP response code "
+ statusCode
+ ", indicating success");
} else {
log.warn(
unit.getId() + " responded with HTTP " + statusCode + ", indicating a unsuccessful kick");
if (statusCode == HttpStatus.SC_FORBIDDEN || statusCode == HttpStatus.SC_UNAUTHORIZED) {
return new KickResponse(
false,
"TCP/HTTP-kick to "
+ crUrl
+ " (user:"
+ crUser
+ ",pass:"
+ crPass
+ ") failed, probably due to wrong user/pass since HTTP response code is "
+ statusCode);
} else {
return new KickResponse(
false, "TCP/HTTP-kick to " + crUrl + " failed with HTTP response code " + statusCode);
}
}
}
private KickResponse kickUsingUDP(Unit unit, String udpCrUrl, String crPass, String crUser) {
try {
String id = String.valueOf(random.nextInt(100000));
String cn = String.valueOf(random.nextLong());
String ts = String.valueOf(System.currentTimeMillis());
String text = ts + id + crUser + cn;
String passFix = crPass == null ? "password" : crPass;
String sig = Crypto.computeHmacAsHexUpperCase(passFix, text);
String req =
"GET http://"
+ udpCrUrl
+ "/?ts="
+ ts
+ "&id="
+ id
+ "&un="
+ crUser
+ "&cn="
+ cn
+ "&sig="
+ sig
+ " HTTP/1.1\r\n\r\n";
byte[] buf = req.getBytes();
if (!udpCrUrl.contains(":")) {
udpCrUrl += ":80";
}
InetAddress address = InetAddress.getByName(udpCrUrl.split(":")[0]);
int port = Integer.parseInt(udpCrUrl.split(":")[1]);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);
for (int i = 0; i < 3; i++) {
MessageStack.push(packet);
}
log.debug(unit.getId() + " has been kicked using UDP (TR-111) method to " + udpCrUrl);
return new KickResponse(true, "UDP kick to " + udpCrUrl + " was initiated");
} catch (Throwable t) {
log.error(unit.getId() + " UDP kick to " + udpCrUrl + " failed", t);
return new KickResponse(false, "UDP kick to " + udpCrUrl + " failed: " + t.getMessage());
}
}
/** KICK RELATED METHODS. */
private String getKeyroot(Unit u) {
for (String paramName : u.getParameters().keySet()) {
if (paramName.startsWith("Device.")) {
return "Device.";
}
if (paramName.startsWith("InternetGatewayDevice.")) {
return "InternetGatewayDevice.";
}
}
throw new RuntimeException(
"No keyroot found for unit " + u.getId() + ", probably because no parameters are defined");
}
private HttpGet authenticate(
DefaultHttpClient client, HttpGet get, String urlStr, String username, String password)
throws MalformedURLException {
URL url = new URL(urlStr);
List<String> authPrefs = new ArrayList<>(2);
authPrefs.add(AuthPolicy.DIGEST);
authPrefs.add(AuthPolicy.BASIC);
client.getParams().setParameter(AuthPNames.PROXY_AUTH_PREF, authPrefs);
client.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authPrefs);
Credentials defaultcreds = new UsernamePasswordCredentials(username, password);
client
.getCredentialsProvider()
.setCredentials(new AuthScope(url.getHost(), url.getPort()), defaultcreds);
return get;
}
}