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}