001package ball.upnp.ssdp; 002/*- 003 * ########################################################################## 004 * UPnP/SSDP Implementation Classes 005 * $Id: SSDPMessage.java 7215 2021-01-03 18:39:51Z ball $ 006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-upnp/trunk/src/main/java/ball/upnp/ssdp/SSDPMessage.java $ 007 * %% 008 * Copyright (C) 2013 - 2021 Allen D. Ball 009 * %% 010 * Licensed under the Apache License, Version 2.0 (the "License"); 011 * you may not use this file except in compliance with the License. 012 * You may obtain a copy of the License at 013 * 014 * http://www.apache.org/licenses/LICENSE-2.0 015 * 016 * Unless required by applicable law or agreed to in writing, software 017 * distributed under the License is distributed on an "AS IS" BASIS, 018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 019 * See the License for the specific language governing permissions and 020 * limitations under the License. 021 * ########################################################################## 022 */ 023import java.net.DatagramPacket; 024import java.net.InetSocketAddress; 025import java.net.SocketAddress; 026import java.net.SocketException; 027import java.net.URI; 028import java.util.Date; 029import java.util.List; 030import java.util.Objects; 031import java.util.function.Function; 032import java.util.regex.Pattern; 033import java.util.stream.Stream; 034import org.apache.http.Header; 035import org.apache.http.HttpHeaders; 036import org.apache.http.HttpMessage; 037import org.apache.http.HttpStatus; 038import org.apache.http.client.utils.DateUtils; 039import org.apache.http.protocol.HttpDateGenerator; 040 041import static java.nio.charset.StandardCharsets.UTF_8; 042import static java.util.concurrent.TimeUnit.MILLISECONDS; 043import static java.util.concurrent.TimeUnit.SECONDS; 044import static java.util.stream.Collectors.toList; 045 046/** 047 * SSDP {@link HttpMessage} interface definition. 048 * 049 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 050 * @version $Revision: 7215 $ 051 */ 052public interface SSDPMessage extends HttpMessage, HttpStatus { 053 054 /** 055 * {@link HttpDateGenerator} instance. 056 */ 057 public static final HttpDateGenerator GENERATOR = new HttpDateGenerator(); 058 059 /** 060 * SSDP message header name. 061 */ 062 public static final String 063 AL = "AL", 064 CACHE_CONTROL = HttpHeaders.CACHE_CONTROL.toUpperCase(), 065 DATE = HttpHeaders.DATE, 066 EXT = "EXT", 067 HOST = HttpHeaders.HOST.toUpperCase(), 068 LOCATION = HttpHeaders.LOCATION.toUpperCase(), 069 MAN = "MAN", 070 MX = "MX", 071 NT = "NT", 072 NTS = "NTS", 073 SERVER = HttpHeaders.SERVER.toUpperCase(), 074 ST = "ST", 075 USN = "USN", 076 BOOTID_UPNP_ORG = "BOOTID.UPNP.ORG", 077 NEXTBOOTID_UPNP_ORG = "NEXTBOOTID.UPNP.ORG", 078 CONFIGID_UPNP_ORG = "CONFIGID.UPNP.ORG", 079 SEARCHPORT_UPNP_ORG = "SEARCHPORT.UPNP.ORG", 080 USER_AGENT = "USER-AGENT", 081 SECURELOCATION_UPNP_ORG = "SECURELOCATION.UPNP.ORG"; 082 083 /** 084 * SSDP {@link #NTS} value. 085 */ 086 public static final String 087 SSDP_ALIVE = "ssdp:alive", 088 SSDP_UPDATE = "ssdp:update", 089 SSDP_BYEBYE = "ssdp:byebye"; 090 091 /** 092 * SSDP {@link #ST} value. 093 */ 094 public static final URI SSDP_ALL = URI.create("ssdp:all"); 095 096 /** 097 * HTTP cache control key. 098 */ 099 public static final String 100 MAX_AGE = "max-age"; 101 102 /** 103 * {@link SSDPMessage} end-of-line sequence. 104 */ 105 public static final String EOL = "\r\n"; 106 107 /** 108 * {@link SSDPMessage} end-of-message sequence. 109 */ 110 public static final String EOM = EOL + EOL; 111 112 /** 113 * Static method to parse a {@link DatagramPacket} to a {@link List} of 114 * lines ({@link String}s). 115 * 116 * @param packet The {@link DatagramPacket}. 117 * 118 * @return The {@link List} of parsed lines. 119 */ 120 public static List<String> parse(DatagramPacket packet) { 121 String string = 122 new String(packet.getData(), 123 packet.getOffset(), packet.getLength(), 124 UTF_8); 125 126 return Pattern.compile(EOL).splitAsStream(string).collect(toList()); 127 } 128 129 /** 130 * Method to get the expiration time for {@link.this} 131 * {@link SSDPMessage}. 132 * 133 * @return The expiration time (milliseconds since the UNIX epoch). 134 */ 135 public long getExpiration(); 136 137 /** 138 * Method to find the first {@link Header} matching {@code names} and 139 * return that value. 140 * 141 * @param names The candidate {@link Header} names. 142 * 143 * @return The value or {@code null} if no header found. 144 */ 145 default String getHeaderValue(String... names) { 146 String string = 147 Stream.of(names) 148 .map(this::getFirstHeader) 149 .filter(Objects::nonNull) 150 .map(Header::getValue) 151 .findFirst().orElse(null); 152 153 return string; 154 } 155 156 /** 157 * Method to find the first {@link Header} matching {@code names} and 158 * return the value converted with {@code function}. 159 * 160 * @param <T> The target type. 161 * @param function The conversion {@code Function}. 162 * @param names The candidate {@link Header} names. 163 * 164 * @return The converted value or {@code null} if no header found. 165 */ 166 default <T> T getHeaderValue(Function<String,T> function, 167 String... names) { 168 String string = getHeaderValue(names); 169 170 return (string != null) ? function.apply(string) : null; 171 } 172 173 /** 174 * Method to find the first {@link Header} matching {@code name} with a 175 * parameter matching {@code parameter} and return that parameter value. 176 * 177 * @param name The target {@link Header} name. 178 * @param parameter The target parameter name. 179 * 180 * @return The value or {@code null} if no header/parameter combination 181 * is found. 182 */ 183 default String getHeaderParameterValue(String name, String parameter) { 184 String value = 185 Stream.of(getHeaders(name)) 186 .map(t -> t.getElements()) 187 .filter(Objects::nonNull) 188 .flatMap(t -> Stream.of(t)) 189 .filter(t -> parameter.equals(t.getName())) 190 .map(t -> t.getValue()) 191 .filter(Objects::nonNull) 192 .findFirst().orElse(null); 193 194 return value; 195 } 196 197 /** 198 * Method to find the first {@link Header} matching {@code name} with a 199 * parameter matching {@code parameter} and return that parameter value 200 * converted with {@code function}. 201 * 202 * @param <T> The target type. 203 * @param name The target {@link Header} name. 204 * @param parameter The target parameter name. 205 * 206 * @return The value or {@code null} if no header/parameter combination 207 * is found. 208 */ 209 default <T> T getHeaderParameterValue(Function<String,T> function, 210 String name, String parameter) { 211 String string = getHeaderParameterValue(name, parameter); 212 213 return (string != null) ? function.apply(string) : null; 214 } 215 216 /** 217 * Method to get the {@value #NT} {@link URI}. 218 * 219 * @return The {@value #NT} {@link URI}. 220 */ 221 default URI getNT() { return getHeaderValue(URI::create, NT); } 222 223 /** 224 * Method to get the {@value #ST} {@link URI}. 225 * 226 * @return The {@value #ST} {@link URI}. 227 */ 228 default URI getST() { return getHeaderValue(URI::create, ST); } 229 230 /** 231 * Method to get the {@value #USN} {@link URI}. 232 * 233 * @return The {@value #USN} {@link URI}. 234 */ 235 default URI getUSN() { return getHeaderValue(URI::create, USN); } 236 237 /** 238 * Method to get the location {@link URI}. 239 * 240 * @return The service location {@link URI}. 241 */ 242 default URI getLocation() { 243 return getHeaderValue(URI::create, LOCATION, AL); 244 } 245 246 /** 247 * Implementation method for {@link #getExpiration()}. 248 * 249 * @param message The {@link SSDPMessage}. 250 * @param timestamp The message's timestamp. 251 * 252 * @return The expiration time (milliseconds since the UNIX epoch). 253 */ 254 public static long getExpiration(SSDPMessage message, long timestamp) { 255 long expiration = timestamp; 256 Date date = message.getHeaderValue(DateUtils::parseDate, DATE); 257 258 if (date != null) { 259 expiration = date.getTime(); 260 } 261 262 Long maxAge = 263 message.getHeaderParameterValue(Long::decode, 264 CACHE_CONTROL, MAX_AGE); 265 266 expiration += 267 MILLISECONDS.convert((maxAge != null) ? maxAge : 0, SECONDS); 268 269 return expiration; 270 } 271}