View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.transport.impl;
17  
18  import org.fourthline.cling.model.ModelUtil;
19  import org.fourthline.cling.model.message.StreamRequestMessage;
20  import org.fourthline.cling.model.message.StreamResponseMessage;
21  import org.fourthline.cling.model.message.UpnpHeaders;
22  import org.fourthline.cling.model.message.UpnpMessage;
23  import org.fourthline.cling.model.message.UpnpRequest;
24  import org.fourthline.cling.model.message.UpnpResponse;
25  import org.fourthline.cling.model.message.header.UpnpHeader;
26  import org.fourthline.cling.transport.spi.InitializationException;
27  import org.fourthline.cling.transport.spi.StreamClient;
28  import org.seamless.http.Headers;
29  import org.seamless.util.Exceptions;
30  import org.seamless.util.URIUtil;
31  import org.seamless.util.io.IO;
32  
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.net.HttpURLConnection;
36  import java.net.ProtocolException;
37  import java.net.SocketTimeoutException;
38  import java.net.URL;
39  import java.net.URLStreamHandlerFactory;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.logging.Level;
43  import java.util.logging.Logger;
44  
45  /**
46   * Default implementation based on the JDK's <code>HttpURLConnection</code>.
47   * <p>
48   * This class works around a serious design issue in the SUN JDK, so it will not work on any JDK that
49   * doesn't offer the <code>sun.net.www.protocol.http.HttpURLConnection </code> implementation.
50   * </p>
51   * <p>
52   * This implementation <em>DOES NOT WORK</em> on Android. Read the Cling manual for
53   * alternatives for Android.
54   * </p>
55   * <p>
56   * This implementation <em>DOES NOT</em> support Cling's server-side heartbeat for connection checking.
57   * Any data returned by a server has to be "valid HTTP", checked in Sun's HttpClient with:
58   * </p>
59   * {@code ret = b[0] == 'H' && b[1] == 'T' && b[2] == 'T' && b[3] == 'P' && b[4] == '/' && b[5] == '1' && b[6] == '.';}
60   * <p>
61   * Hence, if you are using this client, don't call Cling's
62   * {@link org.fourthline.cling.model.profile.RemoteClientInfo#isRequestCancelled()} function on your
63   * server to send a heartbeat to the client!
64   * </p>
65   *
66   * @author Christian Bauer
67   */
68  public class StreamClientImpl implements StreamClient {
69  
70      final static String HACK_STREAM_HANDLER_SYSTEM_PROPERTY = "hackStreamHandlerProperty";
71  
72      final private static Logger log = Logger.getLogger(StreamClient.class.getName());
73  
74      final protected StreamClientConfigurationImpl configuration;
75  
76      public StreamClientImpl(StreamClientConfigurationImpl configuration) throws InitializationException {
77          this.configuration = configuration;
78  
79          if (ModelUtil.ANDROID_EMULATOR || ModelUtil.ANDROID_RUNTIME) {
80              /*
81              See the fantastic PERMITTED_USER_METHODS here:
82  
83              https://android.googlesource.com/platform/libcore/+/android-4.0.1_r1.2/luni/src/main/java/java/net/HttpURLConnection.java
84  
85              We'd have to basically copy the whole Android code, and have a dependency on
86              libcore.*, and do much more hacking to allow more HTTP methods. This is the same
87              problem we are hacking below for the JDK but at least there we don't have a
88              dependency issue for compiling Cling. These guys all suck, there is no list
89              of "permitted" HTTP methods. HttpURLConnection and the whole stream handler
90              factory stuff is the worst Java API ever created.
91              */
92              throw new InitializationException(
93                  "This client does not work on Android. The design of HttpURLConnection is broken, we "
94                      + "can not add additional 'permitted' HTTP methods. Read the Cling manual."
95              );
96          }
97  
98          log.fine("Using persistent HTTP stream client connections: " + configuration.isUsePersistentConnections());
99          System.setProperty("http.keepAlive", Boolean.toString(configuration.isUsePersistentConnections()));
100 
101         // Hack the environment to allow additional HTTP methods
102         if (System.getProperty(HACK_STREAM_HANDLER_SYSTEM_PROPERTY) == null) {
103             log.fine("Setting custom static URLStreamHandlerFactory to work around bad JDK defaults");
104             try {
105                 // Use reflection to avoid dependency on sun.net package so this class at least
106                 // loads on Android, even if it doesn't work...
107                 URL.setURLStreamHandlerFactory(
108                     (URLStreamHandlerFactory) Class.forName(
109                         "org.fourthline.cling.transport.impl.FixedSunURLStreamHandler"
110                     ).newInstance()
111                 );
112             } catch (Throwable t) {
113                 throw new InitializationException(
114                     "Failed to set modified URLStreamHandlerFactory in this environment."
115                         + " Can't use bundled default client based on HTTPURLConnection, see manual."
116                 );
117             }
118             System.setProperty(HACK_STREAM_HANDLER_SYSTEM_PROPERTY, "alreadyWorkedAroundTheEvilJDK");
119         }
120     }
121 
122     @Override
123     public StreamClientConfigurationImpl getConfiguration() {
124         return configuration;
125     }
126 
127     @Override
128     public StreamResponseMessage sendRequest(StreamRequestMessage requestMessage) {
129 
130         final UpnpRequest requestOperation = requestMessage.getOperation();
131         log.fine("Preparing HTTP request message with method '" + requestOperation.getHttpMethodName() + "': " + requestMessage);
132 
133         URL url = URIUtil.toURL(requestOperation.getURI());
134 
135         HttpURLConnection urlConnection = null;
136         InputStream inputStream;
137         try {
138 
139             urlConnection = (HttpURLConnection) url.openConnection();
140 
141             urlConnection.setRequestMethod(requestOperation.getHttpMethodName());
142 
143             // Use the built-in expiration, we can't cancel HttpURLConnection
144             urlConnection.setReadTimeout(configuration.getTimeoutSeconds() * 1000);
145             urlConnection.setConnectTimeout(configuration.getTimeoutSeconds() * 1000);
146 
147             applyRequestProperties(urlConnection, requestMessage);
148             applyRequestBody(urlConnection, requestMessage);
149 
150             log.fine("Sending HTTP request: " + requestMessage);
151             inputStream = urlConnection.getInputStream();
152             return createResponse(urlConnection, inputStream);
153 
154         } catch (ProtocolException ex) {
155             log.log(Level.WARNING, "HTTP request failed: " + requestMessage, Exceptions.unwrap(ex));
156             return null;
157         } catch (IOException ex) {
158 
159             if (urlConnection == null) {
160                 log.log(Level.WARNING, "HTTP request failed: " + requestMessage, Exceptions.unwrap(ex));
161                 return null;
162             }
163 
164             if (ex instanceof SocketTimeoutException) {
165                 log.info(
166                     "Timeout of " + getConfiguration().getTimeoutSeconds()
167                         + " seconds while waiting for HTTP request to complete, aborting: " + requestMessage
168                 );
169                 return null;
170             }
171 
172             if (log.isLoggable(Level.FINE))
173                 log.fine("Exception occurred, trying to read the error stream: " + Exceptions.unwrap(ex));
174             try {
175                 inputStream = urlConnection.getErrorStream();
176                 return createResponse(urlConnection, inputStream);
177             } catch (Exception errorEx) {
178                 if (log.isLoggable(Level.FINE))
179                     log.fine("Could not read error stream: " + errorEx);
180                 return null;
181             }
182         } catch (Exception ex) {
183             log.log(Level.WARNING, "HTTP request failed: " + requestMessage, Exceptions.unwrap(ex));
184             return null;
185 
186         } finally {
187 
188             if (urlConnection != null) {
189                 // Release any idle persistent connection, or "indicate that we don't want to use this server for a while"
190                 urlConnection.disconnect();
191             }
192         }
193     }
194 
195     @Override
196     public void stop() {
197         // NOOP
198     }
199 
200     protected void applyRequestProperties(HttpURLConnection urlConnection, StreamRequestMessage requestMessage) {
201 
202         urlConnection.setInstanceFollowRedirects(false); // Defaults to true but not needed here
203 
204         // HttpURLConnection always adds a "Host" header
205 
206         // HttpURLConnection always adds an "Accept" header (not needed but shouldn't hurt)
207 
208         // Add the default user agent if not already set on the message
209         if (!requestMessage.getHeaders().containsKey(UpnpHeader.Type.USER_AGENT)) {
210             urlConnection.setRequestProperty(
211                 UpnpHeader.Type.USER_AGENT.getHttpName(),
212                 getConfiguration().getUserAgentValue(requestMessage.getUdaMajorVersion(), requestMessage.getUdaMinorVersion())
213             );
214         }
215 
216         // Other headers
217         applyHeaders(urlConnection, requestMessage.getHeaders());
218     }
219 
220     protected void applyHeaders(HttpURLConnection urlConnection, Headers headers) {
221         log.fine("Writing headers on HttpURLConnection: " + headers.size());
222         for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
223             for (String v : entry.getValue()) {
224                 String headerName = entry.getKey();
225                 log.fine("Setting header '" + headerName + "': " + v);
226                 urlConnection.setRequestProperty(headerName, v);
227             }
228         }
229     }
230 
231     protected void applyRequestBody(HttpURLConnection urlConnection, StreamRequestMessage requestMessage) throws IOException {
232 
233         if (requestMessage.hasBody()) {
234             urlConnection.setDoOutput(true);
235         } else {
236             urlConnection.setDoOutput(false);
237             return;
238         }
239 
240         if (requestMessage.getBodyType().equals(UpnpMessage.BodyType.STRING)) {
241             IO.writeUTF8(urlConnection.getOutputStream(), requestMessage.getBodyString());
242         } else if (requestMessage.getBodyType().equals(UpnpMessage.BodyType.BYTES)) {
243             IO.writeBytes(urlConnection.getOutputStream(), requestMessage.getBodyBytes());
244         }
245         urlConnection.getOutputStream().flush();
246     }
247 
248     protected StreamResponseMessage createResponse(HttpURLConnection urlConnection, InputStream inputStream) throws Exception {
249 
250         if (urlConnection.getResponseCode() == -1) {
251             log.warning("Received an invalid HTTP response: " + urlConnection.getURL());
252             log.warning("Is your Cling-based server sending connection heartbeats with " +
253                 "RemoteClientInfo#isRequestCancelled? This client can't handle " +
254                 "heartbeats, read the manual.");
255             return null;
256         }
257 
258         // Status
259         UpnpResponse responseOperation = new UpnpResponse(urlConnection.getResponseCode(), urlConnection.getResponseMessage());
260 
261         log.fine("Received response: " + responseOperation);
262 
263         // Message
264         StreamResponseMessage responseMessage = new StreamResponseMessage(responseOperation);
265 
266         // Headers
267         responseMessage.setHeaders(new UpnpHeaders(urlConnection.getHeaderFields()));
268 
269         // Body
270         byte[] bodyBytes = null;
271         InputStream is = null;
272         try {
273             is = inputStream;
274             if (inputStream != null) bodyBytes = IO.readBytes(is);
275         } finally {
276             if (is != null)
277                 is.close();
278         }
279 
280         if (bodyBytes != null && bodyBytes.length > 0 && responseMessage.isContentTypeMissingOrText()) {
281 
282             log.fine("Response contains textual entity body, converting then setting string on message");
283             responseMessage.setBodyCharacters(bodyBytes);
284 
285         } else if (bodyBytes != null && bodyBytes.length > 0) {
286 
287             log.fine("Response contains binary entity body, setting bytes on message");
288             responseMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes);
289 
290         } else {
291             log.fine("Response did not contain entity body");
292         }
293 
294         log.fine("Response message complete: " + responseMessage);
295         return responseMessage;
296     }
297 
298 }
299 
300