/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.io.comm;
import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import freenet.io.AddressIdentifier;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.transport.ip.HostnameSyntaxException;
import freenet.support.transport.ip.HostnameUtil;
import freenet.support.transport.ip.IPUtil;
/**
* Long-term InetAddress. If created with an IP address, then the IP address is primary.
* If created with a name, then the name is primary, and the IP address can change.
* Most code ripped from Peer.
*
* Propagates the IP address on equals() but not the hostname. This does not change
* hashCode() because it only happens if hostname is set, and in that case, hashCode()
* is based on the hostname and not on the IP address. So it is safe to put
* FreenetInetAddress's into hashtables: neither equals() nor getAddress() will change
* its hashCode.
*
* BUT a FreenetInetAddress with IP 1.2.3.4 and no hostname is *NOT* equal to one with
* the IP address and no name. So if you want to match on only the IP address, you need
* to either call dropHostname() first (after which neither propagation nor getAddress()
* will change the hashcode), or just use InetAddress's.
*
* FIXME reconsider whether we need this. The lazy lookup is useful but not THAT useful,
* and we have a regular lookup task now anyway. Over-complex, could lead to odd bugs,
* although not if used correctly as explained above.
* @author amphibian
*/
public class FreenetInetAddress {
private static volatile boolean logMINOR;
private static volatile boolean logDEBUG;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
logDEBUG = Logger.shouldLog(LogLevel.DEBUG, this);
}
});
}
// hostname - only set if we were created with a hostname
// and not an address
private final String hostname;
private InetAddress _address;
/**
* Create from serialized form on a DataInputStream.
*/
public FreenetInetAddress(DataInput dis) throws IOException {
int firstByte = dis.readUnsignedByte();
byte[] ba;
if(firstByte == 255) {
if(logMINOR) Logger.minor(this, "New format IPv6 address");
// New format IPv6 address
ba = new byte[16];
dis.readFully(ba);
} else if(firstByte == 0) {
if(logMINOR) Logger.minor(this, "New format IPv4 address");
// New format IPv4 address
ba = new byte[4];
dis.readFully(ba);
} else {
throw new IOException("Unknown type byte (old form? corrupt stream? too short/long prev field?): "+firstByte);
}
_address = InetAddress.getByAddress(ba);
String name = null;
String s = dis.readUTF();
if(s.length() > 0)
name = s;
hostname = name;
}
/**
* Create from serialized form on a DataInputStream.
*/
public FreenetInetAddress(DataInput dis, boolean checkHostnameOrIPSyntax) throws HostnameSyntaxException,
IOException {
int firstByte = dis.readUnsignedByte();
byte[] ba;
if(firstByte == 255) {
if(logMINOR) Logger.minor(this, "New format IPv6 address");
// New format IPv6 address
ba = new byte[16];
dis.readFully(ba);
} else if(firstByte == 0) {
if(logMINOR) Logger.minor(this, "New format IPv4 address");
// New format IPv4 address
ba = new byte[4];
dis.readFully(ba);
} else {
// Old format IPv4 address
ba = new byte[4];
ba[0] = (byte)firstByte;
dis.readFully(ba, 1, 3);
}
_address = InetAddress.getByAddress(ba);
String name = null;
String s = dis.readUTF();
if(s.length() > 0)
name = s;
hostname = name;
if(checkHostnameOrIPSyntax && null != hostname) {
if(!HostnameUtil.isValidHostname(hostname, true)) throw new HostnameSyntaxException();
}
}
/**
* Create from an InetAddress. The IP address is primary i.e. fixed.
* The hostname either doesn't exist, or is looked up.
*/
public FreenetInetAddress(InetAddress address) {
_address = address;
hostname = null;
}
public FreenetInetAddress(String host, boolean allowUnknown) throws UnknownHostException {
InetAddress addr = null;
if(host != null){
if(host.startsWith("/")) host = host.substring(1);
host = host.trim();
}
// if we were created with an explicit IP address, use it as such
// debugging log messages because AddressIdentifier doesn't appear to handle all IPv6 literals correctly, such as "fe80::204:1234:dead:beef"
AddressIdentifier.AddressType addressType = AddressIdentifier.getAddressType(host);
if(logDEBUG) Logger.debug(this, "Address type of '"+host+"' appears to be '"+addressType+ '\'');
if(addressType != AddressIdentifier.AddressType.OTHER) {
// Is an IP address
addr = InetAddress.getByName(host);
// Don't catch UnknownHostException here, if it happens there's a bug in AddressIdentifier.
if(logDEBUG) Logger.debug(this, "host is '"+host+"' and addr.getHostAddress() is '"+addr.getHostAddress()+ '\'');
if(addr != null) {
host = null;
} else {
addr = null;
}
}
if( addr == null ) {
if(logDEBUG) Logger.debug(this, '\'' +host+"' does not look like an IP address");
}
this._address = addr;
this.hostname = host;
// we're created with a hostname so delay the lookup of the address
// until it's needed to work better with dynamic DNS hostnames
}
public FreenetInetAddress(String host, boolean allowUnknown, boolean checkHostnameOrIPSyntax) throws HostnameSyntaxException, UnknownHostException {
InetAddress addr = null;
if(host != null){
if(host.startsWith("/")) host = host.substring(1);
host = host.trim();
}
// if we were created with an explicit IP address, use it as such
// debugging log messages because AddressIdentifier doesn't appear to handle all IPv6 literals correctly, such as "fe80::204:1234:dead:beef"
AddressIdentifier.AddressType addressType = AddressIdentifier.getAddressType(host);
if(logDEBUG) Logger.debug(this, "Address type of '"+host+"' appears to be '"+addressType+ '\'');
if(addressType != AddressIdentifier.AddressType.OTHER) {
try {
addr = InetAddress.getByName(host);
} catch (UnknownHostException e) {
if(!allowUnknown) throw e;
addr = null;
}
if(logDEBUG) Logger.debug(this, "host is '"+host+"' and addr.getHostAddress() is '"+(addr != null ? addr.getHostAddress()+ '\'' : ""));
if(addr != null && addr.getHostAddress().equals(host)) {
if(logDEBUG) Logger.debug(this, '\'' +host+"' looks like an IP address");
host = null;
} else {
addr = null;
}
}
if( addr == null ) {
if(logDEBUG) Logger.debug(this, '\'' +host+"' does not look like an IP address");
}
this._address = addr;
this.hostname = host;
if(checkHostnameOrIPSyntax && null != this.hostname) {
if(!HostnameUtil.isValidHostname(this.hostname, true)) throw new HostnameSyntaxException();
}
// we're created with a hostname so delay the lookup of the address
// until it's needed to work better with dynamic DNS hostnames
}
public boolean laxEquals(FreenetInetAddress addr) {
if(hostname != null) {
if(addr.hostname == null) {
if(_address == null) return false; // No basis for comparison.
if(addr._address != null) {
return _address.equals(addr._address);
}
} else {
if (!hostname.equalsIgnoreCase(addr.hostname)) {
return false;
}
// Now that we know we have the same hostname, we can propagate the IP.
if((_address != null) && (addr._address == null))
addr._address = _address;
if((addr._address != null) && (_address == null))
_address = addr._address;
// Except if we actually do have two different looked-up IPs!
if((addr._address != null) && (_address != null) && !addr._address.equals(_address))
return false;
// Equal.
return true;
}
}
// His hostname might not be null. Not a problem.
return _address.equals(addr._address);
}
@Override
public boolean equals(Object o) {
if(!(o instanceof FreenetInetAddress)) {
return false;
}
FreenetInetAddress addr = (FreenetInetAddress)o;
if(hostname != null) {
if(addr.hostname == null)
return false;
if (!hostname.equalsIgnoreCase(addr.hostname)) {
return false;
}
// Now that we know we have the same hostname, we can propagate the IP.
if((_address != null) && (addr._address == null))
addr._address = _address;
if((addr._address != null) && (_address == null))
_address = addr._address;
// Except if we actually do have two different looked-up IPs!
if((addr._address != null) && (_address != null) && !addr._address.equals(_address))
return false;
// Equal.
return true;
}
if(addr.hostname != null)
return false;
// No hostname, go by address.
if(!_address.equals(addr._address)) {
return false;
}
return true;
}
public boolean strictEquals(FreenetInetAddress addr) {
if(hostname != null) {
if(addr.hostname == null)
return false;
if (!hostname.equalsIgnoreCase(addr.hostname)) {
return false;
}
// Now that we know we have the same hostname, we can propagate the IP.
if((_address != null) && (addr._address == null))
addr._address = _address;
if((addr._address != null) && (_address == null))
_address = addr._address;
// Except if we actually do have two different looked-up IPs!
if((addr._address != null) && (_address != null) && !addr._address.equals(_address))
return false;
// Equal.
return true;
} else if(addr.hostname != null /* && hostname == null */) {
return false;
}
// No hostname, go by address.
if(!getHostName(_address).equalsIgnoreCase(getHostName(addr._address))) {
//Logger.minor(this, "Addresses do not match: mine="+getHostName(_address)+" his="+getHostName(addr._address));
return false;
}
return true;
}
/**
* Get the IP address. Look it up if necessary, but return the last value if it
* has ever been looked up before; will not trigger a new lookup if it has been
* looked up before.
*/
public InetAddress getAddress() {
return getAddress(true);
}
/**
* Get the IP address. Look it up only if allowed to, but return the last value if it
* has ever been looked up before; will not trigger a new lookup if it has been
* looked up before.
*/
public InetAddress getAddress(boolean doDNSRequest) {
if (_address != null) {
return _address;
} else {
if(!doDNSRequest) return null;
InetAddress addr = getHandshakeAddress();
if( addr != null ) {
this._address = addr;
}
return addr;
}
}
/**
* Get the IP address, looking up the hostname if the hostname is primary, even if
* it has been looked up before. Typically called on a reconnect attempt, when the
* dyndns address may have changed.
*/
public InetAddress getHandshakeAddress() {
// Since we're handshaking, hostname-to-IP may have changed
if ((_address != null) && (hostname == null)) {
if(logMINOR) Logger.minor(this, "hostname is null, returning "+_address);
return _address;
} else {
if(logMINOR) Logger.minor(this, "Looking up '"+hostname+"' in DNS", new Exception("debug"));
/*
* Peers are constructed from an address once a
* handshake has been completed, so this lookup
* will only be performed during a handshake
* (this method should normally only be called
* from PeerNode.getHandshakeIPs() and once
* each connection from this.getAddress()
* otherwise) - it doesn't mean we perform a
* DNS lookup with every packet we send.
*/
try {
InetAddress addr = InetAddress.getByName(hostname);
if(logMINOR) Logger.minor(this, "Look up got '"+addr+ '\'');
if( addr != null ) {
/*
* cache the answer since getHandshakeAddress()
* doesn't use the cached value, thus
* getHandshakeIPs() should always get the
* latest value from DNS (minus Java's caching)
*/
this._address = InetAddress.getByAddress(addr.getAddress());
if(logMINOR) Logger.minor(this, "Setting address to "+_address);
}
return addr;
} catch (UnknownHostException e) {
if(logMINOR) Logger.minor(this, "DNS said hostname '"+hostname+"' is an unknown host, returning null");
return null;
}
}
}
@Override
public int hashCode() {
if(hostname != null) {
return hostname.hashCode(); // Was set at creation, so it can safely be used here.
} else {
return _address.hashCode(); // Can be null, but if so, hostname will be non-null.
}
}
@Override
public String toString() {
if(hostname != null) {
return hostname;
} else {
return _address.getHostAddress();
}
}
public String toStringPrefNumeric() {
if(_address != null)
return _address.getHostAddress();
else
return hostname;
}
public void writeToDataOutputStream(DataOutputStream dos) throws IOException {
InetAddress addr = this.getAddress();
if (addr == null) throw new UnknownHostException();
byte[] data = addr.getAddress();
if(data.length == 4)
dos.write(0);
else
dos.write(255);
dos.write(data);
if(hostname != null)
dos.writeUTF(hostname);
else
dos.writeUTF("");
}
/**
* Return the hostname or the IP address of the given InetAddress.
* Does not attempt to do a reverse lookup; if the hostname is
* known, return it, otherwise return the textual IP address.
*/
public static String getHostName(InetAddress primaryIPAddress) {
if(primaryIPAddress == null) return null;
String s = primaryIPAddress.toString();
String addr = s.substring(0, s.indexOf('/')).trim();
if(addr.length() == 0)
return primaryIPAddress.getHostAddress();
else
return addr;
}
public boolean isRealInternetAddress(boolean lookup, boolean defaultVal, boolean allowLocalAddresses) {
if(_address != null) {
return IPUtil.isValidAddress(_address, allowLocalAddresses);
} else {
if(lookup) {
InetAddress a = getAddress();
if(a != null)
return IPUtil.isValidAddress(a, allowLocalAddresses);
}
return defaultVal;
}
}
/**
* Get a new <code>FreenetInetAddress</code> with host name removed.
*
* @return a new <code>FreenetInetAddress</code> with host name removed; or {@code null} if no
* known ip address is associated with this object. You may want to do a
* <code>getAddress(true)</code> before calling this.
*/
public FreenetInetAddress dropHostname() {
if(_address == null) {
Logger.error(this, "Can't dropHostname() if no address!");
return null;
}
if(hostname != null) {
return new FreenetInetAddress(_address);
} else return this;
}
public boolean hasHostnameNoIP() {
return hostname != null && hostname.length() > 0 && _address == null;
}
public boolean isIPv6(boolean defaultValue) {
if(_address == null)
return defaultValue;
else
return (_address instanceof Inet6Address);
}
}