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}