Skip to content
This repository has been archived by the owner on May 7, 2020. It is now read-only.

Implemented Option to choose default network interface #3930

Merged
merged 8 commits into from Aug 11, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -4,7 +4,7 @@ Bundle-Name: Eclipse SmartHome Config Core
Bundle-SymbolicName: org.eclipse.smarthome.config.core
Bundle-Version: 0.9.0.qualifier
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.google.common.base,
com.google.common.collect,
com.google.gson,
Expand All @@ -21,6 +21,7 @@ Import-Package: com.google.common.base,
org.eclipse.smarthome.core.common.registry,
org.eclipse.smarthome.core.events,
org.eclipse.smarthome.core.i18n,
org.eclipse.smarthome.core.net,
org.osgi.framework,
org.osgi.service.component,
org.osgi.service.component.annotations;resolution:=optional,
Expand Down
@@ -1 +1,2 @@
/org.eclipse.smarthome.config.core.internal.i18n.I18nConfigOptionsProvider.xml
/org.eclipse.smarthome.config.core.net.internal.NetworkConfigOptionProvider.xml
@@ -0,0 +1,97 @@
/**
* Copyright (c) 2014-2017 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.eclipse.smarthome.config.core.net.internal;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.eclipse.smarthome.config.core.ConfigOptionProvider;
import org.eclipse.smarthome.config.core.ParameterOption;
import org.eclipse.smarthome.core.net.NetUtil;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Provides a list of IPv4 addresses of the local machine and shows the user which interface belongs to which IP address
*
* @author Stefan Triller - initial contribution
*
*/
@Component
public class NetworkConfigOptionProvider implements ConfigOptionProvider {

static final URI CONFIG_URI = URI.create("system:network");
static final String PARAM_PRIMARY_ADDRESS = "primaryAddress";

private final Logger logger = LoggerFactory.getLogger(NetworkConfigOptionProvider.class);

@Override
public Collection<ParameterOption> getParameterOptions(URI uri, String param, Locale locale) {
if (!uri.equals(CONFIG_URI)) {
return null;
}

if (param.equals(PARAM_PRIMARY_ADDRESS)) {
return getIPv4Addresses();
}
return null;
}

private List<ParameterOption> getIPv4Addresses() {
List<ParameterOption> interfaceOptions = new ArrayList<>();

Set<String> subnets = new HashSet<>();

try {
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
final NetworkInterface current = interfaces.nextElement();
if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
continue;
}

for (InterfaceAddress ifAddr : current.getInterfaceAddresses()) {
InetAddress addr = ifAddr.getAddress();

if (addr.isLoopbackAddress() || (addr instanceof Inet6Address)) {
continue;
}

String ipv4Address = addr.getHostAddress();
String subNetString = NetUtil.getIpv4NetAddress(ipv4Address, ifAddr.getNetworkPrefixLength()) + "/"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you do if this now throws the newly introduced IllegalArgumentException?

+ String.valueOf(ifAddr.getNetworkPrefixLength());

subnets.add(subNetString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although you didn't add any annotation to NetUtil.getIpv4NetAddress, I assume that subNetString can be null here.

}
}
} catch (SocketException ex) {
logger.error("Could not retrieve network interface: {}", ex.getMessage(), ex);
return null;
}

for (String subnet : subnets) {
ParameterOption po = new ParameterOption(subnet, subnet);
interfaceOptions.add(po);
}

return interfaceOptions;
}

}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="http://eclipse.org/smarthome/schemas/config-description/v1.0.0"
xsi:schemaLocation="http://eclipse.org/smarthome/schemas/config-description/v1.0.0
http://eclipse.org/smarthome/schemas/config-description-1.0.0.xsd">

<config-description uri="system:network">
<parameter name="primaryAddress" type="text">
<label>Primary Address</label>
<description>A subnet (e.g. 192.168.1.0/24) <!-- or an IP address (e.g. 192.168.1.5) --></description>
<limitToOptions>true</limitToOptions>
</parameter>
</config-description>

</config-description:config-descriptions>
Expand Up @@ -28,7 +28,7 @@ Ignore-Package: org.eclipse.smarthome.core.internal.items,org.eclipse.smarthome.
nal,org.eclipse.smarthome.core.internal.events,org.eclipse.smarthome.core.internal.loggin
g
Bundle-Name: Eclipse SmartHome Core
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-Version: 0.9.0.qualifier
Bundle-ManifestVersion: 2
Expand Down
@@ -0,0 +1 @@
/org.eclipse.smarthome.network.xml
Expand Up @@ -13,10 +13,17 @@
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.eclipse.jdt.annotation.NonNull;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -25,17 +32,65 @@
*
* @author Markus Rathgeb - Initial contribution and API
* @author Mark Herwege - Added methods to find broadcast address(es)
* @author Stefan Triller - Converted to OSGi service with primary ipv4 conf
*/
public class NetUtil {
@Component(name = "org.eclipse.smarthome.network", property = { "service.config.description.uri=system:network",
"service.config.label=Network Settings", "service.config.category=system" })
public class NetUtil implements NetworkAddressService {

private static final String PRIMARY_ADDRESS = "primaryAddress";
private static final Logger LOGGER = LoggerFactory.getLogger(NetUtil.class);

private NetUtil() {
private static final Pattern IPV4_PATTERN = Pattern
.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");

private String primaryAddress;

@SuppressWarnings("unchecked")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why that?

protected void activate(ComponentContext componentContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of pulling the properties out of the context, why don't you simply implement activate(Map<String, Object> props)?

Dictionary<String, Object> props = componentContext.getProperties();
modified((Map<String, Object>) props);
}

@Modified
public synchronized void modified(Map<String, Object> config) {
String primaryAddressConf = (String) config.get(PRIMARY_ADDRESS);
if (primaryAddressConf == null || primaryAddressConf.isEmpty() || !isValidIPConfig(primaryAddressConf)) {
// if none is specified we return the default one for backward compatibility
LOGGER.warn("Non valid IP configuration found, will continue to use first interface");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As >90% of users will have exactly one interface, there is no need to set this configuration parameter.
Why should you bother them with a warning?
You should only show a warning if the parameter is invalid.

primaryAddress = NetUtil.getLocalIpv4HostAddress();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are calling a deprecated method here. Should be avoided :-)

} else {
primaryAddress = primaryAddressConf;
}
}

@Override
public String getPrimaryIpv4HostAddress() {
String primaryIP;

String[] addrString = primaryAddress.split("/");
if (addrString.length > 1) {
String ip = getIPv4inSubnet(primaryAddress);
if (ip == null) {
// an error has occurred, using first interface like nothing has been configured
LOGGER.warn("Invalid address '{}', will use first interface instead.", primaryAddress);
primaryIP = NetUtil.getLocalIpv4HostAddress();
} else {
primaryIP = ip;
}
} else {
primaryIP = addrString[0];
}

return primaryIP;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you convince your IDE that primaryIP is definitely not null here?

}

/**
* Deprecated: Please use the NetworkAddressService with getPrimaryIpv4HostAddress()
*
* Get the first candidate for a local IPv4 host address (non loopback, non localhost).
*/
@Deprecated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a message about what people should now use instead.

public static String getLocalIpv4HostAddress() {
try {
String hostAddress = null;
Expand Down Expand Up @@ -104,4 +159,107 @@ public static String getBroadcastAddress() {
}
}

/**
* Converts a netmask in bits into a string representation
* i.e. 24 bits -> 255.255.255.0
*
* @param prefixLength bits of the netmask
* @return string representation of netmask (i.e. 255.255.255.0)
*/
public static @NonNull String networkPrefixLengthToNetmask(int prefixLength) {
if (prefixLength > 31 || prefixLength < 1) {
throw new IllegalArgumentException("Network prefix length is not within bounds");
}

int ipv4Netmask = 0xFFFFFFFF;
ipv4Netmask <<= (32 - prefixLength);

byte[] octets = new byte[] { (byte) (ipv4Netmask >>> 24), (byte) (ipv4Netmask >>> 16),
(byte) (ipv4Netmask >>> 8), (byte) ipv4Netmask };

String result = "";
for (int i = 0; i < 4; i++) {
result += octets[i] & 0xff;
if (i < 3) {
result += ".";
}
}
return result;
}

/**
* Get the network address a specific ip address is in
*
* @param ipAddressString ipv4 address of the device (i.e. 192.168.5.1)
* @param netMask netmask in bits (i.e. 24)
* @return network a device is in (i.e. 192.168.5.0)
*/
public static @NonNull String getIpv4NetAddress(@NonNull String ipAddressString, short netMask) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to assure to always return valid non-null results, you should at least also throw IllegalArgumentExceptions. Or what would you return if I pass you "ABC" as my ipAddressString?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is a wonderful candidate for a couple of unit tests!

String subnetMaskString = networkPrefixLengthToNetmask(netMask);

String[] netMaskOctets = subnetMaskString.split("\\.");
String[] ipv4AddressOctets = ipAddressString.split("\\.");
String netAddress = "";
for (int i = 0; i < 4; i++) {
netAddress += Integer.parseInt(ipv4AddressOctets[i]) & Integer.parseInt(netMaskOctets[i]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to catch NumberFormatException - this should not bubble up in the call stack.

if (i < 3) {
netAddress += ".";
}
}
return netAddress;
}

private String getIPv4inSubnet(String subnet) {
try {
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
final NetworkInterface current = interfaces.nextElement();
if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
continue;
}

for (InterfaceAddress ifAddr : current.getInterfaceAddresses()) {
InetAddress addr = ifAddr.getAddress();

if (addr.isLoopbackAddress() || (addr instanceof Inet6Address)) {
continue;
}

String ipv4Address = addr.getHostAddress();
String subNetString = getIpv4NetAddress(ipv4Address, ifAddr.getNetworkPrefixLength()) + "/"
+ String.valueOf(ifAddr.getNetworkPrefixLength());

// use first IP within this subnet
if (subNetString.equals(subnet)) {
return ipv4Address;
}
}
}
} catch (SocketException ex) {
LOGGER.error("Could not retrieve network interface: {}", ex.getMessage(), ex);
}
return null;
}

private boolean isValidIPConfig(String ipConfig) {

if (ipConfig.contains("/")) {
String parts[] = ipConfig.split("/");
boolean ipMatches = IPV4_PATTERN.matcher(parts[0]).matches();

int netMask = Integer.parseInt(parts[1]);
boolean netMaskMatches = false;
if (netMask > 0 || netMask < 32) {
netMaskMatches = true;
}

if (ipMatches && netMaskMatches) {
return true;
}
} else {
return IPV4_PATTERN.matcher(ipConfig).matches();
}
return false;
}

}
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2014-2017 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.eclipse.smarthome.core.net;

import org.eclipse.jdt.annotation.Nullable;

/**
* Interface that provides access to configured network addresses
*
* @author Stefan Triller - initial contribution
*
*/
public interface NetworkAddressService {

@Nullable
String getPrimaryIpv4HostAddress();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add JavaDoc

}
3 changes: 2 additions & 1 deletion docs/_includes/documentation-menu.html
Expand Up @@ -16,6 +16,7 @@
<li><a href="{{docu}}/features/rest.html">REST API</a></li>
<li><a href="{{docu}}/features/dsl.html">Textual Configuration</a></li>
<li><a href="{{docu}}/features/internationalization.html">Internationalization</a></li>
<li><a href="{{docu}}/features/frameworkUtilities.html">Framework Utilities</a></li>
<li><a href="{{docu}}/features/rules.html">Rules</a></li>
<li><a href="{{docu}}/features/bindings/hue/readme.html">Bindings</a>
<ul>
Expand Down Expand Up @@ -69,4 +70,4 @@
<li><a href="{{docu}}/community/contributing.html">Contributing</a></li>
<li><a href="{{docu}}/community/downloads.html">Downloads</a></li>
</ul></li>
</ul>
</ul>