001package jmri.util.node; 002 003import java.io.File; 004import java.io.FileOutputStream; 005import java.io.IOException; 006import java.io.OutputStreamWriter; 007import java.io.Writer; 008import java.net.InetAddress; 009import java.net.NetworkInterface; 010import java.net.SocketException; 011import java.net.UnknownHostException; 012import java.nio.charset.StandardCharsets; 013import java.util.ArrayList; 014import java.util.Enumeration; 015import java.util.HashMap; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Map; 019import java.util.Set; 020import java.util.UUID; 021import java.util.concurrent.ThreadLocalRandom; 022 023import jmri.profile.Profile; 024import jmri.profile.ProfileManager; 025import jmri.util.FileUtil; 026import org.jdom2.Document; 027import org.jdom2.Element; 028import org.jdom2.JDOMException; 029import org.jdom2.input.SAXBuilder; 030import org.jdom2.output.Format; 031import org.jdom2.output.XMLOutputter; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035/** 036 * Provide unique identities for JMRI. 037 * <p> 038 * A list of former identities is retained to aid in migrating from the former 039 * identity to the new identity. 040 * <p> 041 * Currently the storageIdentity is a randomly generated UUID, that is also used 042 * for backwards compatibility with JMRI 4.14. If we find a reliable 043 * cross-platform mechanism to tie that to the machine's unique identity (from 044 * the CPU or motherboard), not from a NIC, this may change. If a JMRI 4.14 045 * generated UUID is available, it is retained and used as the storageIdentity. 046 * 047 * @author Randall Wood (C) 2013, 2014, 2016 048 * @author Dave Heap (C) 2018 049 */ 050public class NodeIdentity { 051 052 private final Set<String> formerIdentities = new HashSet<>(); 053 private UUID uuid = null; 054 private String networkIdentity = null; 055 private String storageIdentity = null; 056 private final Map<Profile, String> profileStorageIdentities = new HashMap<>(); 057 058 private static NodeIdentity instance = null; 059 private static final Logger log = LoggerFactory.getLogger(NodeIdentity.class); 060 061 private static final String ROOT_ELEMENT = "nodeIdentityConfig"; // NOI18N 062 private static final String UUID_ELEMENT = "uuid"; // NOI18N 063 private static final String NODE_IDENTITY = "nodeIdentity"; // NOI18N 064 private static final String STORAGE_IDENTITY = "storageIdentity"; // NOI18N 065 private static final String FORMER_IDENTITIES = "formerIdentities"; // NOI18N 066 private static final String IDENTITY_PREFIX = "jmri-"; 067 068 /** 069 * A string of 64 URL compatible characters. 070 * <p> 071 * Used by {@link #uuidToCompactString uuidToCompactString} and 072 * {@link #uuidFromCompactString uuidFromCompactString}. 073 */ 074 protected static final String URL_SAFE_CHARACTERS = 075 "abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789"; // NOI18N 076 077 private NodeIdentity() { 078 init(); // initialize as a method so the initialization can be 079 // synchronized. 080 } 081 082 private synchronized void init() { 083 File identityFile = this.identityFile(); 084 if (identityFile.exists()) { 085 try { 086 boolean save = false; 087 this.formerIdentities.clear(); 088 Document doc = (new SAXBuilder()).build(identityFile); 089 Element ue = doc.getRootElement().getChild(UUID_ELEMENT); 090 if (ue != null) { 091 try { 092 String attr = ue.getAttributeValue(UUID_ELEMENT); 093 this.uuid = uuidFromCompactString(attr); 094 // backwards compatible, see class docs 095 this.storageIdentity = this.uuid.toString(); 096 this.formerIdentities.add(this.storageIdentity); 097 this.formerIdentities.add(IDENTITY_PREFIX + attr); 098 } catch (IllegalArgumentException ex) { 099 // do nothing 100 } 101 } 102 Element si = doc.getRootElement().getChild(STORAGE_IDENTITY); 103 if (si != null) { 104 try { 105 this.storageIdentity = si.getAttributeValue(STORAGE_IDENTITY); 106 if (this.uuid == null || !this.storageIdentity.equals(this.uuid.toString())) { 107 this.uuid = UUID.fromString(this.storageIdentity); 108 save = true; // updated UUID 109 } 110 } catch (IllegalArgumentException ex) { 111 save = true; // save if attribute not available 112 } 113 } else { 114 save = true; // element missing, need to save 115 } 116 if (this.storageIdentity == null) { 117 save = true; 118 this.getStorageIdentity(false); 119 } 120 String id = null; 121 try { 122 id = doc.getRootElement().getChild(NODE_IDENTITY).getAttributeValue(NODE_IDENTITY); 123 doc.getRootElement().getChild(FORMER_IDENTITIES).getChildren().stream() 124 .forEach(e -> this.formerIdentities.add(e.getAttributeValue(NODE_IDENTITY))); 125 } catch (NullPointerException ex) { 126 // do nothing -- if id was not set, it will be generated 127 } 128 if (!this.validateNetworkIdentity(id)) { 129 log.warn("Node identity {} is invalid. Generating new node identity.", id); 130 save = true; 131 this.getNetworkIdentity(false); 132 } else { 133 this.networkIdentity = id; 134 } 135 // save if new identities were created or expected attribute did 136 // not exist 137 if (save) { 138 this.saveIdentity(); 139 } 140 } catch ( 141 JDOMException | 142 IOException ex) { 143 log.error("Unable to read node identities: {}", ex.getLocalizedMessage()); 144 this.getNetworkIdentity(true); 145 } 146 } else { 147 this.getNetworkIdentity(true); 148 } 149 } 150 151 /** 152 * Return the node's current network identity. For historical purposes, the 153 * network identity is also referred to as the {@literal node} or 154 * {@literal node identity}. 155 * 156 * @return A network identity. If this identity is not in the form 157 * {@code jmri-MACADDRESS-profileId}, or if {@code MACADDRESS} is a 158 * multicast MAC address, this identity should be considered 159 * unreliable and subject to change across JMRI restarts. Note that 160 * if the identity is in the form {@code jmri-MACADDRESS} the JMRI 161 * instance has not loaded a configuration profile, and the network 162 * identity will change once that a configuration profile is loaded. 163 */ 164 public static synchronized String networkIdentity() { 165 String uniqueId = ""; 166 Profile profile = ProfileManager.getDefault().getActiveProfile(); 167 if (profile != null) { 168 uniqueId = "-" + profile.getUniqueId(); 169 } 170 if (instance == null) { 171 instance = new NodeIdentity(); 172 log.info("Using {}{} as the JMRI Node identity", instance.getNetworkIdentity(), uniqueId); 173 } 174 return instance.getNetworkIdentity() + uniqueId; 175 } 176 177 /** 178 * Return the node's current storage identity for the active profile. This 179 * is a convenience method that calls {@link #storageIdentity(Profile)} with 180 * the result of {@link jmri.profile.ProfileManager#getActiveProfile()}. 181 * 182 * @return A storage identity. 183 * @see #storageIdentity(Profile) 184 */ 185 public static synchronized String storageIdentity() { 186 return storageIdentity(ProfileManager.getDefault().getActiveProfile()); 187 } 188 189 /** 190 * Return the node's current storage identity. This can be used in networked 191 * file systems to ensure per-computer storage is available. 192 * <p> 193 * <strong>Note</strong> this only ensure uniqueness if the preferences path 194 * is not shared between multiple computers as documented in 195 * {@link jmri.util.FileUtil#getPreferencesPath()} (the most common cause of 196 * this would be sharing a user's home directory in its entirety between two 197 * computers with similar operating systems as noted in 198 * getPreferencesPath()). 199 * 200 * @param profile The profile to get the identity for. This is only needed 201 * to check that the identity should not be in an older 202 * format. 203 * @return A storage identity. If this identity is not in the form of a UUID 204 * or {@code jmri-UUID-profileId}, this identity should be 205 * considered unreliable and subject to change across JMRI restarts. 206 * When generating a new storage ID, the form is always a UUID and 207 * other forms are used only to ensure continuity where other forms 208 * may have been used in the past. 209 */ 210 public static synchronized String storageIdentity(Profile profile) { 211 if (instance == null) { 212 instance = new NodeIdentity(); 213 } 214 String id = instance.getStorageIdentity(); 215 // this entire check is so that a JMRI 4.14 style identity string can be 216 // built 217 // and checked against the given profile to determine if that should be 218 // used 219 // instead of just returning the non-profile-specific machine identity 220 if (profile != null) { 221 // using a map to store profile-specific identities allows for the 222 // possibility 223 // that, although there is only one active profile at a time, other 224 // profiles 225 // may be manipulated by JMRI while that profile is active (this 226 // happens to a 227 // limited extent already in the profile configuration UI) 228 // (a map also allows for ensuring the info message is displayed 229 // once per profile) 230 if (!instance.profileStorageIdentities.containsKey(profile)) { 231 String oldId = IDENTITY_PREFIX + uuidToCompactString(instance.uuid) + "-" + profile.getUniqueId(); 232 File local = new File(new File(profile.getPath(), Profile.PROFILE), oldId); 233 if (local.exists() && local.isDirectory()) { 234 id = oldId; 235 } 236 instance.profileStorageIdentities.put(profile, id); 237 log.info("Using {} as the JMRI storage identity for profile id {}", id, profile.getUniqueId()); 238 } 239 id = instance.profileStorageIdentities.get(profile); 240 } 241 return id; 242 } 243 244 /** 245 * If network hardware on a node was replaced, the identity will change. 246 * 247 * @return A list of other identities this node may have had in the past. 248 */ 249 public static synchronized List<String> formerIdentities() { 250 if (instance == null) { 251 instance = new NodeIdentity(); 252 log.info("Using {} as the JMRI Node identity", instance.getNetworkIdentity()); 253 } 254 return instance.getFormerIdentities(); 255 } 256 257 /** 258 * Verify that the current identity is a valid identity for this hardware. 259 * 260 * @param identity the identity to validate; may be null 261 * @return true if the identity is based on this hardware; false otherwise 262 */ 263 private synchronized boolean validateNetworkIdentity(String identity) { 264 try { 265 Enumeration<NetworkInterface> enumeration = NetworkInterface.getNetworkInterfaces(); 266 while (enumeration.hasMoreElements()) { 267 NetworkInterface nic = enumeration.nextElement(); 268 if (!nic.isVirtual() && !nic.isLoopback()) { 269 String nicIdentity = this.createNetworkIdentity(nic.getHardwareAddress()); 270 if (nicIdentity != null && nicIdentity.equals(identity)) { 271 return true; 272 } 273 } 274 } 275 } catch (SocketException ex) { 276 log.error("Error accessing interface", ex); 277 } 278 return false; 279 } 280 281 /** 282 * Get a node identity from the current hardware. 283 * 284 * @param save whether to save this identity or not 285 */ 286 private synchronized void getNetworkIdentity(boolean save) { 287 try { 288 NetworkInterface.getByInetAddress(InetAddress.getLocalHost()); 289 try { 290 this.networkIdentity = this.createNetworkIdentity( 291 NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress()); 292 } catch (NullPointerException ex) { 293 // NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress() 294 // failed. 295 // This can be due to multiple reasons, most likely 296 // getLocalHost() failing on certain platforms. 297 // Only set networkIdentity to null, since the following null 298 // checks address all potential problems 299 // with getLocalHost() including some expected conditions (such 300 // as InetAddress.getLocalHost() 301 // returning the loopback interface). 302 this.networkIdentity = null; 303 } 304 if (this.networkIdentity == null) { 305 Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces(); 306 while (nics.hasMoreElements()) { 307 NetworkInterface nic = nics.nextElement(); 308 if (!nic.isLoopback() && !nic.isVirtual() && (nic.getHardwareAddress() != null)) { 309 this.networkIdentity = this.createNetworkIdentity(nic.getHardwareAddress()); 310 if (this.networkIdentity != null) { 311 break; 312 } 313 } 314 } 315 } 316 } catch ( 317 SocketException | 318 UnknownHostException ex) { 319 this.networkIdentity = null; 320 } 321 if (this.networkIdentity == null) { 322 log.info("No MAC addresses found, generating a random multicast MAC address as per RFC 4122."); 323 byte[] randBytes = new byte[6]; 324 ThreadLocalRandom.current().nextBytes(randBytes); 325 // set multicast bit in first octet 326 randBytes[0] = (byte) (randBytes[0] | 0x01); 327 this.networkIdentity = this.createNetworkIdentity(randBytes); 328 } 329 this.formerIdentities.add(this.networkIdentity); 330 if (save) { 331 this.saveIdentity(); 332 } 333 } 334 335 /** 336 * Get a node identity from the current hardware. 337 * 338 * @param save whether to save this identity or not 339 */ 340 private synchronized void getStorageIdentity(boolean save) { 341 if (this.storageIdentity == null) { 342 // also generate UUID to protect against case where user 343 // migrates from JMRI < 4.14 to JMRI > 4.14 back to JMRI = 4.14 344 if (this.uuid == null) { 345 this.uuid = UUID.randomUUID(); 346 } 347 this.storageIdentity = this.uuid.toString(); 348 this.formerIdentities.add(this.storageIdentity); 349 } 350 if (save) { 351 this.saveIdentity(); 352 } 353 } 354 355 /** 356 * Save the current node identity and all former identities to file. 357 */ 358 private void saveIdentity() { 359 Document doc = new Document(); 360 doc.setRootElement(new Element(ROOT_ELEMENT)); 361 Element networkIdentityElement = new Element(NODE_IDENTITY); 362 Element storageIdentityElement = new Element(STORAGE_IDENTITY); 363 Element formerIdentitiesElement = new Element(FORMER_IDENTITIES); 364 Element uuidElement = new Element(UUID_ELEMENT); 365 if (this.networkIdentity == null) { 366 this.getNetworkIdentity(false); 367 } 368 if (this.storageIdentity == null) { 369 this.getStorageIdentity(false); 370 } 371 // ensure formerIdentities contains current identities as well 372 this.formerIdentities.add(this.networkIdentity); 373 this.formerIdentities.add(this.storageIdentity); 374 if (this.uuid != null) { 375 this.formerIdentities.add(IDENTITY_PREFIX + uuidToCompactString(this.uuid)); 376 } 377 networkIdentityElement.setAttribute(NODE_IDENTITY, this.networkIdentity); 378 storageIdentityElement.setAttribute(STORAGE_IDENTITY, this.storageIdentity); 379 this.formerIdentities.stream().forEach(formerIdentity -> { 380 log.debug("Retaining former node identity {}", formerIdentity); 381 Element e = new Element(NODE_IDENTITY); 382 e.setAttribute(NODE_IDENTITY, formerIdentity); 383 formerIdentitiesElement.addContent(e); 384 }); 385 doc.getRootElement().addContent(networkIdentityElement); 386 doc.getRootElement().addContent(storageIdentityElement); 387 if (this.uuid != null) { 388 uuidElement.setAttribute(UUID_ELEMENT, uuidToCompactString(this.uuid)); 389 doc.getRootElement().addContent(uuidElement); 390 } 391 doc.getRootElement().addContent(formerIdentitiesElement); 392 try (Writer w = new OutputStreamWriter(new FileOutputStream(this.identityFile()), StandardCharsets.UTF_8)) { 393 XMLOutputter fmt = new XMLOutputter(); 394 fmt.setFormat(Format.getPrettyFormat() 395 .setLineSeparator(System.getProperty("line.separator")) 396 .setTextMode(Format.TextMode.PRESERVE)); 397 fmt.output(doc, w); 398 } catch (IOException ex) { 399 log.error("Unable to store node identities: {}", ex.getLocalizedMessage()); 400 } 401 } 402 403 /** 404 * Create an identity string given a MAC address. 405 * 406 * @param mac a byte array representing a MAC address. 407 * @return An identity or null if input is null. 408 */ 409 private String createNetworkIdentity(byte[] mac) { 410 StringBuilder sb = new StringBuilder(IDENTITY_PREFIX); // NOI18N 411 try { 412 for (int i = 0; i < mac.length; i++) { 413 sb.append(String.format("%02X", mac[i])); // NOI18N 414 } 415 } catch (NullPointerException ex) { 416 return null; 417 } 418 return sb.toString(); 419 } 420 421 private File identityFile() { 422 return new File(FileUtil.getPreferencesPath() + "nodeIdentity.xml"); // NOI18N 423 } 424 425 /** 426 * @return the network identity 427 */ 428 private synchronized String getNetworkIdentity() { 429 if (this.networkIdentity == null) { 430 this.getNetworkIdentity(false); 431 } 432 return this.networkIdentity; 433 } 434 435 /** 436 * @return the storage identity 437 */ 438 private synchronized String getStorageIdentity() { 439 if (this.storageIdentity == null) { 440 this.getStorageIdentity(false); 441 } 442 return this.storageIdentity; 443 } 444 445 /** 446 * Encodes a UUID into a 22 character URL compatible string. This is used to 447 * store the UUID in a manner compatible with JMRI 4.14. 448 * <p> 449 * From an example by <a href="https://stackoverflow.com/">Tom Lobato</a>. 450 * 451 * @param uuid the UUID to encode 452 * @return the 22 character string 453 */ 454 protected static String uuidToCompactString(UUID uuid) { 455 char[] c = new char[22]; 456 long buffer = 0; 457 int val6; 458 StringBuilder sb = new StringBuilder(); 459 460 for (int i = 1; i <= 22; i++) { 461 switch (i) { 462 case 1: 463 buffer = uuid.getLeastSignificantBits(); 464 break; 465 case 12: 466 buffer = uuid.getMostSignificantBits(); 467 break; 468 default: 469 break; 470 } 471 val6 = (int) (buffer & 0x3F); 472 c[22 - i] = URL_SAFE_CHARACTERS.charAt(val6); 473 buffer = buffer >>> 6; 474 } 475 return sb.append(c).toString(); 476 } 477 478 /** 479 * Decodes the original UUID from a 22 character string generated by 480 * {@link #uuidToCompactString uuidToCompactString}. This is used to store 481 * the UUID in a manner compatible with JMRI 4.14. 482 * 483 * @param compact the 22 character string 484 * @return the original UUID 485 */ 486 protected static UUID uuidFromCompactString(String compact) { 487 long mostSigBits = 0; 488 long leastSigBits = 0; 489 long buffer = 0; 490 int val6; 491 492 for (int i = 0; i <= 21; i++) { 493 switch (i) { 494 case 0: 495 buffer = 0; 496 break; 497 case 11: 498 mostSigBits = buffer; 499 buffer = 0; 500 break; 501 default: 502 buffer = buffer << 6; 503 break; 504 } 505 val6 = URL_SAFE_CHARACTERS.indexOf(compact.charAt(i)); 506 buffer = buffer | (val6 & 0x3F); 507 } 508 leastSigBits = buffer; 509 return new UUID(mostSigBits, leastSigBits); 510 } 511 512 /** 513 * @return the former identities; this is a combination of former network 514 * and storage identities 515 */ 516 public List<String> getFormerIdentities() { 517 return new ArrayList<>(this.formerIdentities); 518 } 519}