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}