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.upnp.ssdp.SSDPResponse; 022import ball.xml.XalanConstants; 023import java.io.BufferedReader; 024import java.io.ByteArrayOutputStream; 025import java.io.IOException; 026import java.io.InputStreamReader; 027import java.net.Inet4Address; 028import java.net.Inet6Address; 029import java.net.InetAddress; 030import java.net.InetSocketAddress; 031import java.net.InterfaceAddress; 032import java.net.NetworkInterface; 033import java.net.SocketAddress; 034import java.time.Duration; 035import java.time.Instant; 036import java.util.LinkedHashSet; 037import java.util.List; 038import java.util.Set; 039import java.util.TreeSet; 040import java.util.stream.Stream; 041import javax.annotation.PostConstruct; 042import javax.annotation.PreDestroy; 043import javax.xml.namespace.QName; 044import javax.xml.parsers.DocumentBuilderFactory; 045import javax.xml.transform.Transformer; 046import javax.xml.transform.TransformerFactory; 047import javax.xml.transform.dom.DOMSource; 048import javax.xml.transform.stream.StreamResult; 049import javax.xml.xpath.XPath; 050import javax.xml.xpath.XPathFactory; 051import lombok.EqualsAndHashCode; 052import lombok.NoArgsConstructor; 053import lombok.RequiredArgsConstructor; 054import lombok.ToString; 055import lombok.extern.log4j.Log4j2; 056import org.springframework.beans.factory.annotation.Autowired; 057import org.springframework.boot.context.event.ApplicationReadyEvent; 058import org.springframework.context.event.EventListener; 059import org.springframework.http.MediaType; 060import org.springframework.scheduling.annotation.Scheduled; 061import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 062import org.springframework.stereotype.Service; 063import org.springframework.web.bind.annotation.PathVariable; 064import org.springframework.web.bind.annotation.RequestMapping; 065import org.springframework.web.bind.annotation.RestController; 066import org.w3c.dom.Document; 067import org.w3c.dom.NodeList; 068 069import static java.lang.ProcessBuilder.Redirect.PIPE; 070import static java.nio.charset.StandardCharsets.UTF_8; 071import static java.util.stream.Collectors.toList; 072import static javax.xml.transform.OutputKeys.INDENT; 073import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION; 074import static javax.xml.xpath.XPathConstants.NODESET; 075import static javax.xml.xpath.XPathConstants.NUMBER; 076 077/** 078 * {@link InetAddress} to {@code nmap} output {@link Document} 079 * {@link java.util.Map}. 080 * 081 * {@injected.fields} 082 * 083 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 084 */ 085@RestController 086@RequestMapping(value = { "/network/nmap/" }, 087 produces = MediaType.APPLICATION_XML_VALUE) 088@Service 089@NoArgsConstructor @Log4j2 090public class Nmap extends InetAddressMap<Document> implements XalanConstants { 091 private static final long serialVersionUID = -1039375546367339774L; 092 093 private static final Duration INTERVAL = Duration.ofMinutes(60); 094 095 private static final String NMAP = "nmap"; 096 private static final List<String> NMAP_ARGV = 097 Stream.of(NMAP, "--no-stylesheet", "-oX", "-", "-n", "-PS", "-A") 098 .collect(toList()); 099 100 /** @serial */ @Autowired private NetworkInterfaces interfaces = null; 101 /** @serial */ @Autowired private ARPCache arp = null; 102 /** @serial */ @Autowired private SSDP ssdp = null; 103 /** @serial */ @Autowired private ThreadPoolTaskExecutor executor = null; 104 /** @serial */ private DocumentBuilderFactory factory = null; 105 /** @serial */ private XPath xpath = null; 106 /** @serial */ private Transformer transformer = null; 107 /** @serial */ private boolean disabled = true; 108 109 @PostConstruct 110 public void init() throws Exception { 111 factory = DocumentBuilderFactory.newInstance(); 112 113 xpath = XPathFactory.newInstance().newXPath(); 114 115 transformer = TransformerFactory.newInstance().newTransformer(); 116 transformer.setOutputProperty(OMIT_XML_DECLARATION, NO); 117 transformer.setOutputProperty(INDENT, YES); 118 transformer.setOutputProperty(XALAN_INDENT_AMOUNT.toString(), String.valueOf(2)); 119 120 try { 121 var argv = Stream.of(NMAP, "-version").collect(toList()); 122 123 log.info("{}", argv); 124 125 var process = 126 new ProcessBuilder(argv) 127 .inheritIO() 128 .redirectOutput(PIPE) 129 .start(); 130 131 try (var in = process.getInputStream()) { 132 new BufferedReader(new InputStreamReader(in, UTF_8)).lines() 133 .forEach(t -> log.info("{}", t)); 134 } 135 136 disabled = (process.waitFor() != 0); 137 } catch (Exception exception) { 138 disabled = true; 139 } 140 141 if (disabled) { 142 log.warn("nmap command is not available"); 143 } 144 } 145 146 @PreDestroy 147 public void destroy() { } 148 149 @EventListener(ApplicationReadyEvent.class) 150 @Scheduled(fixedDelay = 30 * 1000) 151 public void update() { 152 if (! isDisabled()) { 153 try { 154 var empty = factory.newDocumentBuilder().newDocument(); 155 156 empty.appendChild(empty.createElement("nmaprun")); 157 158 interfaces.stream() 159 .map(NetworkInterface::getInterfaceAddresses) 160 .flatMap(List::stream) 161 .map(InterfaceAddress::getAddress) 162 .filter(t -> (! t.isMulticastAddress())) 163 .forEach(t -> putIfAbsent(t, empty)); 164 165 arp.keySet().stream() 166 .filter(t -> (! t.isMulticastAddress())) 167 .forEach(t -> putIfAbsent(t, empty)); 168 169 ssdp.values().stream() 170 .filter(t -> t instanceof SSDPResponse) 171 .map(t -> ((SSDPResponse) t).getSocketAddress()) 172 .map(t -> ((InetSocketAddress) t).getAddress()) 173 .forEach(t -> putIfAbsent(t, empty)); 174 175 keySet().stream() 176 .filter(t -> INTERVAL.compareTo(getOutputAge(t)) < 0) 177 .map(Worker::new) 178 .forEach(t -> executor.execute(t)); 179 } catch (Exception exception) { 180 log.error("{}", exception.getMessage(), exception); 181 } 182 } 183 } 184 185 @RequestMapping(value = { "{ip}.xml" }) 186 public String nmap(@PathVariable String ip) throws Exception { 187 var out = new ByteArrayOutputStream(); 188 189 transformer.transform(new DOMSource(get(InetAddress.getByName(ip))), new StreamResult(out)); 190 191 return out.toString(UTF_8.name()); 192 } 193 194 public boolean isDisabled() { return disabled; } 195 196 public Set<Integer> getPorts(InetAddress key) { 197 var ports = new TreeSet<Integer>(); 198 var list = (NodeList) get(key, "/nmaprun/host/ports/port/@portid", NODESET); 199 200 if (list != null) { 201 for (int i = 0; i < list.getLength(); i += 1) { 202 ports.add(Integer.parseInt(list.item(i).getNodeValue())); 203 } 204 } 205 206 return ports; 207 } 208 209 public Set<String> getProducts(InetAddress key) { 210 var products = new LinkedHashSet<String>(); 211 var list = (NodeList) get(key, "/nmaprun/host/ports/port/service/@product", NODESET); 212 213 if (list != null) { 214 for (int i = 0; i < list.getLength(); i += 1) { 215 products.add(list.item(i).getNodeValue()); 216 } 217 } 218 219 return products; 220 } 221 222 private Duration getOutputAge(InetAddress key) { 223 long start = 0; 224 var number = (Number) get(key, "/nmaprun/runstats/finished/@time", NUMBER); 225 226 if (number != null) { 227 start = number.longValue(); 228 } 229 230 return Duration.between(Instant.ofEpochSecond(start), Instant.now()); 231 } 232 233 private Object get(InetAddress key, String expression, QName qname) { 234 Object object = null; 235 var document = get(key); 236 237 if (document != null) { 238 try { 239 object = xpath.compile(expression).evaluate(document, qname); 240 } catch (Exception exception) { 241 log.error("{}", exception.getMessage(), exception); 242 } 243 } 244 245 return object; 246 } 247 248 @RequiredArgsConstructor @EqualsAndHashCode @ToString 249 private class Worker implements Runnable { 250 private final InetAddress key; 251 252 @Override 253 public void run() { 254 try { 255 var argv = NMAP_ARGV.stream().collect(toList()); 256 257 if (key instanceof Inet4Address) { 258 argv.add("-4"); 259 } else if (key instanceof Inet6Address) { 260 argv.add("-6"); 261 } 262 263 argv.add(key.getHostAddress()); 264 265 var builder = factory.newDocumentBuilder(); 266 var process = 267 new ProcessBuilder(argv) 268 .inheritIO() 269 .redirectOutput(PIPE) 270 .start(); 271 272 try (var in = process.getInputStream()) { 273 put(key, builder.parse(in)); 274 275 int status = process.waitFor(); 276 277 if (status != 0) { 278 throw new IOException(argv + " returned exit status " + status); 279 } 280 } 281 } catch (Exception exception) { 282 remove(key); 283 log.error("{}", exception.getMessage(), exception); 284 } 285 } 286 } 287}