001package ball.upnp.ssdp; 002/*- 003 * ########################################################################## 004 * UPnP/SSDP Implementation Classes 005 * %% 006 * Copyright (C) 2013 - 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 java.net.DatagramPacket; 022import java.net.URI; 023import java.util.Date; 024import java.util.List; 025import java.util.Objects; 026import java.util.function.Function; 027import java.util.regex.Pattern; 028import java.util.stream.Stream; 029import org.apache.hc.client5.http.utils.DateUtils; 030import org.apache.hc.core5.http.Header; 031import org.apache.hc.core5.http.HttpHeaders; 032import org.apache.hc.core5.http.HttpMessage; 033import org.apache.hc.core5.http.message.BasicHeaderValueParser; 034import org.apache.hc.core5.http.message.ParserCursor; 035import org.apache.hc.core5.util.CharArrayBuffer; 036 037import static java.util.concurrent.TimeUnit.MILLISECONDS; 038import static java.util.concurrent.TimeUnit.SECONDS; 039import static java.util.stream.Collectors.toList; 040 041/** 042 * SSDP {@link HttpMessage} interface definition. 043 * 044 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball} 045 */ 046public interface SSDPMessage extends HttpMessage { 047 048 /** 049 * SSDP message header name. 050 */ 051 public static final String 052 AL = "AL", 053 CACHE_CONTROL = HttpHeaders.CACHE_CONTROL.toUpperCase(), 054 DATE = HttpHeaders.DATE, 055 EXT = "EXT", 056 HOST = HttpHeaders.HOST.toUpperCase(), 057 LOCATION = HttpHeaders.LOCATION.toUpperCase(), 058 MAN = "MAN", 059 MX = "MX", 060 NT = "NT", 061 NTS = "NTS", 062 SERVER = HttpHeaders.SERVER.toUpperCase(), 063 ST = "ST", 064 USN = "USN", 065 BOOTID_UPNP_ORG = "BOOTID.UPNP.ORG", 066 NEXTBOOTID_UPNP_ORG = "NEXTBOOTID.UPNP.ORG", 067 CONFIGID_UPNP_ORG = "CONFIGID.UPNP.ORG", 068 SEARCHPORT_UPNP_ORG = "SEARCHPORT.UPNP.ORG", 069 USER_AGENT = "USER-AGENT", 070 SECURELOCATION_UPNP_ORG = "SECURELOCATION.UPNP.ORG"; 071 072 /** 073 * SSDP {@link #NTS} value. 074 */ 075 public static final String 076 SSDP_ALIVE = "ssdp:alive", 077 SSDP_UPDATE = "ssdp:update", 078 SSDP_BYEBYE = "ssdp:byebye"; 079 080 /** 081 * SSDP {@link #ST} value. 082 */ 083 public static final URI SSDP_ALL = URI.create("ssdp:all"); 084 085 /** 086 * HTTP cache control key. 087 */ 088 public static final String 089 MAX_AGE = "max-age"; 090 091 /** 092 * {@link SSDPMessage} end-of-line sequence. 093 */ 094 public static final String EOL = "\r\n"; 095 096 /** 097 * {@link SSDPMessage} end-of-message sequence. 098 */ 099 public static final String EOM = EOL + EOL; 100 101 /** 102 * Static method to parse a {@link DatagramPacket} to a {@link List} of 103 * lines ({@link CharArrayBuffer}s). 104 * 105 * @param packet The {@link DatagramPacket}. 106 * 107 * @return The {@link List} of parsed lines as {@link CharArrayBuffer}s. 108 */ 109 public static List<CharArrayBuffer> parse(DatagramPacket packet) { 110 CharArrayBuffer buffer = toCharArrayBuffer(packet); 111 List<CharArrayBuffer> list = 112 Pattern.compile(EOL).splitAsStream(buffer) 113 .map(SSDPMessage::toCharArrayBuffer) 114 .collect(toList()); 115 116 return list; 117 } 118 119 /** 120 * Static method to convert a {@link DatagramPacket} to a 121 * {@link CharArrayBuffer}. 122 * 123 * @param packet The {@link DatagramPacket}. 124 * 125 * @return The {@link CharArrayBuffer}. 126 */ 127 public static CharArrayBuffer toCharArrayBuffer(DatagramPacket packet) { 128 CharArrayBuffer buffer = new CharArrayBuffer(packet.getLength()); 129 130 buffer.append(packet.getData(), packet.getOffset(), packet.getLength()); 131 132 return buffer; 133 } 134 135 /** 136 * Static method to convert a {@link String} to a {@link CharArrayBuffer}. 137 * 138 * @param string The {@link String}. 139 * 140 * @return The {@link CharArrayBuffer}. 141 */ 142 public static CharArrayBuffer toCharArrayBuffer(String string) { 143 CharArrayBuffer buffer = new CharArrayBuffer(string.length()); 144 145 buffer.append(string); 146 147 return buffer; 148 } 149 150 /** 151 * Method to get the expiration time for {@link.this} {@link SSDPMessage}. 152 * 153 * @return The expiration time (milliseconds since the UNIX epoch). 154 */ 155 public long getExpiration(); 156 157 /** 158 * Method to find the first {@link Header} matching {@code names} and 159 * return that value. 160 * 161 * @param names The candidate {@link Header} names. 162 * 163 * @return The value or {@code null} if no header found. 164 */ 165 default String getHeaderValue(String... names) { 166 String string = 167 Stream.of(names) 168 .map(this::getFirstHeader) 169 .filter(Objects::nonNull) 170 .map(Header::getValue) 171 .findFirst().orElse(null); 172 173 return string; 174 } 175 176 /** 177 * Method to find the first {@link Header} matching {@code names} and 178 * return the value converted with {@code function}. 179 * 180 * @param <T> The target type. 181 * @param function The conversion {@code Function}. 182 * @param names The candidate {@link Header} names. 183 * 184 * @return The converted value or {@code null} if no header found. 185 */ 186 default <T> T getHeaderValue(Function<String,T> function, String... names) { 187 String string = getHeaderValue(names); 188 189 return (string != null) ? function.apply(string) : null; 190 } 191 192 /** 193 * Method to find the first {@link Header} matching {@code name} with a 194 * parameter matching {@code parameter} and return that parameter value. 195 * 196 * @param name The target {@link Header} name. 197 * @param parameter The target parameter name. 198 * 199 * @return The value or {@code null} if no header/parameter combination 200 * is found. 201 */ 202 default String getHeaderParameterValue(String name, String parameter) { 203 String value = 204 Stream.of(getHeaders(name)) 205 .filter(Objects::nonNull) 206 .map(Header::getValue) 207 .map(t -> BasicHeaderValueParser.INSTANCE.parseElements(t, new ParserCursor(0, t.length()))) 208 .filter(Objects::nonNull) 209 .flatMap(Stream::of) 210 .filter(t -> parameter.equalsIgnoreCase(t.getName())) 211 .map(t -> t.getValue()) 212 .filter(Objects::nonNull) 213 .findFirst().orElse(null); 214 215 return value; 216 } 217 218 /** 219 * Method to find the first {@link Header} matching {@code name} with a 220 * parameter matching {@code parameter} and return that parameter value 221 * converted with {@code function}. 222 * 223 * @param <T> The target type. 224 * @param name The target {@link Header} name. 225 * @param parameter The target parameter name. 226 * 227 * @return The value or {@code null} if no header/parameter combination 228 * is found. 229 */ 230 default <T> T getHeaderParameterValue(Function<String,T> function, String name, String parameter) { 231 String string = getHeaderParameterValue(name, parameter); 232 233 return (string != null) ? function.apply(string) : null; 234 } 235 236 /** 237 * Method to get the {@value #NT} {@link URI}. 238 * 239 * @return The {@value #NT} {@link URI}. 240 */ 241 default URI getNT() { return getHeaderValue(URI::create, NT); } 242 243 /** 244 * Method to get the {@value #ST} {@link URI}. 245 * 246 * @return The {@value #ST} {@link URI}. 247 */ 248 default URI getST() { return getHeaderValue(URI::create, ST); } 249 250 /** 251 * Method to get the {@value #USN} {@link URI}. 252 * 253 * @return The {@value #USN} {@link URI}. 254 */ 255 default URI getUSN() { return getHeaderValue(URI::create, USN); } 256 257 /** 258 * Method to get the location {@link URI}. 259 * 260 * @return The service location {@link URI}. 261 */ 262 default URI getLocation() { 263 return getHeaderValue(URI::create, LOCATION, AL); 264 } 265 266 /** 267 * Implementation method for {@link #getExpiration()}. 268 * 269 * @param message The {@link SSDPMessage}. 270 * @param timestamp The message's timestamp. 271 * 272 * @return The expiration time (milliseconds since the UNIX epoch). 273 */ 274 public static long getExpiration(SSDPMessage message, long timestamp) { 275 long expiration = timestamp; 276 Date date = message.getHeaderValue(DateUtils::parseDate, DATE); 277 278 if (date != null) { 279 expiration = date.getTime(); 280 } 281 282 Long maxAge = message.getHeaderParameterValue(Long::decode, CACHE_CONTROL, MAX_AGE); 283 284 expiration += MILLISECONDS.convert((maxAge != null) ? maxAge : 0, SECONDS); 285 286 return expiration; 287 } 288}