001package voyeur;
002/*-
003 * ##########################################################################
004 * Local Area Network Voyeur
005 * %%
006 * Copyright (C) 2019 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *      http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import ball.annotation.CompileTimeCheck;
022import java.io.BufferedReader;
023import java.io.InputStreamReader;
024import java.net.InetAddress;
025import java.nio.file.Files;
026import java.nio.file.Path;
027import java.nio.file.Paths;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030import lombok.NoArgsConstructor;
031import lombok.extern.log4j.Log4j2;
032import org.springframework.boot.context.event.ApplicationReadyEvent;
033import org.springframework.context.event.EventListener;
034import org.springframework.scheduling.annotation.Scheduled;
035import org.springframework.stereotype.Service;
036import voyeur.types.HardwareAddress;
037
038import static java.lang.ProcessBuilder.Redirect.PIPE;
039import static java.nio.charset.StandardCharsets.UTF_8;
040import static java.util.stream.Collectors.toMap;
041
042/**
043 * {@link java.net.InetAddress} to {@link HardwareAddress}
044 * {@link java.util.Map}.
045 *
046 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
047 */
048@Service
049@NoArgsConstructor @Log4j2
050public class ARPCache extends InetAddressMap<HardwareAddress> {
051    private static final long serialVersionUID = 8171059827964411042L;
052
053    private static final Path PATH = Paths.get("/proc/net/arp");
054
055    private static final ProcessBuilder BUILDER =
056        new ProcessBuilder("arp", "-an")
057        .inheritIO()
058        .redirectOutput(PIPE);
059
060    @CompileTimeCheck
061    private static final Pattern PATTERN =
062        Pattern.compile("(?i)^(?<host>.+)"
063                        + " [(](?<inet>[\\p{Digit}.]+)[)]"
064                        + " at (?<mac>[\\p{XDigit}:]+) .*$");
065
066    /** @serial */ private boolean disabled = false;
067
068    @EventListener(ApplicationReadyEvent.class)
069    @Scheduled(fixedDelay = 60 * 1000)
070    public void update() {
071        if (! isDisabled()) {
072            try {
073                var map = Files.exists(PATH) ? parse(PATH) : parse(BUILDER);
074
075                if (map != null) {
076                    putAll(map);
077                    keySet().retainAll(map.keySet());
078                }
079            } catch (Exception exception) {
080                log.error("{}", exception.getMessage(), exception);
081            }
082        }
083    }
084
085    public boolean isDisabled() { return disabled; }
086
087    private ARPCache parse(Path path) throws Exception {
088        var map =
089            Files.lines(path, UTF_8)
090            .skip(1)
091            .map(t -> t.split("[\\p{Space}]+"))
092            .collect(toMap(k -> getInetAddress(k[0]), v -> new HardwareAddress(v[3]), (t, u) -> t, ARPCache::new));
093
094        return map;
095    }
096
097    private ARPCache parse(ProcessBuilder builder) throws Exception {
098        ARPCache map = null;
099
100        try {
101            var process = builder.start();
102
103            try (var in = process.getInputStream()) {
104                map =
105                    new BufferedReader(new InputStreamReader(in, UTF_8)).lines()
106                    .map(PATTERN::matcher)
107                    .filter(Matcher::matches)
108                    .collect(toMap(k -> getInetAddress(k.group("inet")), v -> new HardwareAddress(v.group("mac")),
109                                   (t, u) -> t, ARPCache::new));
110            }
111
112            disabled = (process.waitFor() != 0);
113        } catch (Exception exception) {
114            disabled = true;
115        }
116
117        if (disabled) {
118            log.warn("arp command is not available");
119        }
120
121        return map;
122    }
123
124    private InetAddress getInetAddress(String string) {
125        InetAddress address = null;
126
127        try {
128            address = InetAddress.getByName(string);
129        } catch (RuntimeException exception) {
130            throw exception;
131        } catch (Exception exception) {
132            throw new IllegalStateException(exception);
133        }
134
135        return address;
136    }
137}