View Javadoc

1   /*
2    * Copyright (C) 2011 Teleal GmbH, Switzerland
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Lesser General Public License as
6    * published by the Free Software Foundation, either version 3 of
7    * the License, or (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  
18  package org.teleal.cling.transport.impl;
19  
20  import org.teleal.cling.model.message.StreamRequestMessage;
21  import org.teleal.cling.model.message.StreamResponseMessage;
22  import org.teleal.cling.model.message.UpnpHeaders;
23  import org.teleal.cling.model.message.UpnpMessage;
24  import org.teleal.cling.model.message.UpnpRequest;
25  import org.teleal.cling.model.message.UpnpResponse;
26  import org.teleal.cling.transport.spi.InitializationException;
27  import org.teleal.cling.transport.spi.StreamClient;
28  import org.teleal.common.http.Headers;
29  import org.teleal.common.io.IO;
30  import org.teleal.common.util.URIUtil;
31  import sun.net.www.protocol.http.Handler;
32  
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.OutputStream;
36  import java.net.HttpURLConnection;
37  import java.net.ProtocolException;
38  import java.net.Proxy;
39  import java.net.URL;
40  import java.net.URLStreamHandler;
41  import java.net.URLStreamHandlerFactory;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.logging.Logger;
45  
46  /**
47   * Default implementation based on the JDK's <code>HttpURLConnection</code>.
48   * <p>
49   * This class works around a serious design issue in the SUN JDK, so it will not work on any JDK that
50   * doesn't offer the <code>sun.net.www.protocol.http.HttpURLConnection </code> implementation.
51   * </p>
52   *
53   * @author Christian Bauer
54   */
55  public class StreamClientImpl implements StreamClient {
56  
57      final static String HACK_STREAM_HANDLER_SYSTEM_PROPERTY = "hackStreamHandlerProperty";
58  
59      final private static Logger log = Logger.getLogger(StreamClient.class.getName());
60  
61      final protected StreamClientConfigurationImpl configuration;
62  
63      public StreamClientImpl(StreamClientConfigurationImpl configuration) throws InitializationException {
64          this.configuration = configuration;
65  
66          log.fine("Using persistent HTTP stream client connections: " + configuration.isUsePersistentConnections());
67          System.setProperty("http.keepAlive", Boolean.toString(configuration.isUsePersistentConnections()));
68  
69          // Hack the JDK to allow additional HTTP methods
70          if (System.getProperty(HACK_STREAM_HANDLER_SYSTEM_PROPERTY) == null) {
71              log.fine("Setting custom static URLStreamHandlerFactory to work around Sun JDK bugs");
72              URLStreamHandlerFactory shf =
73                      new URLStreamHandlerFactory() {
74                          public URLStreamHandler createURLStreamHandler(String protocol) {
75                              log.fine("Creating new URLStreamHandler for protocol: " + protocol);
76                              if ("http".equals(protocol)) {
77                                  return new Handler() {
78  
79                                      protected java.net.URLConnection openConnection(URL u) throws IOException {
80                                          return openConnection(u, null);
81                                      }
82  
83                                      protected java.net.URLConnection openConnection(URL u, Proxy p) throws IOException {
84                                          return new UpnpURLConnection(u, this);
85                                      }
86                                  };
87                              } else {
88                                  return null;
89                              }
90                          }
91                      };
92  
93              try {
94                  URL.setURLStreamHandlerFactory(shf);
95              } catch (Throwable t) {
96                  throw new InitializationException(
97                          "URLStreamHandlerFactory already set for this JVM." +
98                                  " Can't use bundled default client based on JDK's HTTPURLConnection." +
99                                  " Consider alternatives, e.g. org.teleal.cling.transport.impl.apache.StreamClientImpl."
100                 );
101             }
102             System.setProperty(HACK_STREAM_HANDLER_SYSTEM_PROPERTY, "alreadyWorkedAroundTheEvilJDK");
103         }
104     }
105 
106     @Override
107     public StreamClientConfigurationImpl getConfiguration() {
108         return configuration;
109     }
110 
111     @Override
112     public StreamResponseMessage sendRequest(StreamRequestMessage requestMessage) {
113 
114         final UpnpRequest requestOperation = requestMessage.getOperation();
115         log.fine("Preparing HTTP request message with method '" + requestOperation.getHttpMethodName() + "': " + requestMessage);
116 
117         URL url = URIUtil.toURL(requestOperation.getURI());
118 
119         HttpURLConnection urlConnection = null;
120         InputStream inputStream;
121         try {
122 
123             urlConnection = (HttpURLConnection) url.openConnection();
124 
125             urlConnection.setRequestMethod(requestOperation.getHttpMethodName());
126             urlConnection.setReadTimeout(configuration.getDataReadTimeoutSeconds() * 1000);
127             urlConnection.setConnectTimeout(configuration.getConnectionTimeoutSeconds() * 1000);
128 
129             applyRequestProperties(urlConnection, requestMessage);
130             applyRequestBody(urlConnection, requestMessage);
131 
132             log.fine("Sending HTTP request: " + requestMessage);
133             inputStream = urlConnection.getInputStream();
134             return createResponse(urlConnection, inputStream);
135 
136         } catch (ProtocolException ex) {
137             log.fine("Unrecoverable HTTP protocol exception: " + ex);
138             return null;
139         } catch (IOException ex) {
140 
141             if (urlConnection == null) {
142                 log.info("Could not open URL connection: " + ex.getMessage());
143                 return null;
144             }
145 
146             log.fine("Exception occured, trying to read the error stream");
147             try {
148                 inputStream = urlConnection.getErrorStream();
149                 return createResponse(urlConnection, inputStream);
150             } catch (Exception errorEx) {
151                 log.fine("Could not read error stream: " + errorEx);
152                 return null;
153             }
154         } catch (Exception ex) {
155             log.info("Unrecoverable exception occured, no error response possible: " + ex);
156             return null;
157 
158         } finally {
159 
160             if (urlConnection != null) {
161                 // Release any idle persistent connection, or "indicate that we don't want to use this server for a while"
162                 urlConnection.disconnect();
163             }
164         }
165     }
166 
167     @Override
168     public void stop() {
169         // NOOP
170     }
171 
172     protected void applyRequestProperties(HttpURLConnection urlConnection, StreamRequestMessage requestMessage) {
173 
174         urlConnection.setInstanceFollowRedirects(false); // Defaults to true but not needed here
175 
176         // HttpURLConnection always adds a "Host" header
177 
178         // HttpURLConnection always adds an "Accept" header (not needed but shouldn't hurt)
179 
180         // Let's just add the user-agent header on every request, the UDA 1.0 spec doesn't care and the UDA 1.1 spec says OK
181         urlConnection.setRequestProperty(
182                 "User-Agent",
183                 getConfiguration().getUserAgentValue(requestMessage.getUdaMajorVersion(), requestMessage.getUdaMinorVersion())
184         );
185 
186         // Other headers
187         applyHeaders(urlConnection, requestMessage.getHeaders());
188     }
189 
190     protected void applyHeaders(HttpURLConnection urlConnection, Headers headers) {
191         log.fine("Writing headers on HttpURLConnection: " + headers.size());
192         for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
193             for (String v : entry.getValue()) {
194                 String headerName = entry.getKey();
195                 log.fine("Setting header '" + headerName + "': " + v);
196                 urlConnection.setRequestProperty(headerName, v);
197             }
198         }
199     }
200 
201     protected void applyRequestBody(HttpURLConnection urlConnection, StreamRequestMessage requestMessage) throws IOException {
202 
203         if (requestMessage.hasBody()) {
204             urlConnection.setDoOutput(true);
205         } else {
206             urlConnection.setDoOutput(false);
207             return;
208         }
209 
210         if (requestMessage.getBodyType().equals(UpnpMessage.BodyType.STRING)) {
211             IO.writeUTF8(urlConnection.getOutputStream(), requestMessage.getBodyString());
212         } else if (requestMessage.getBodyType().equals(UpnpMessage.BodyType.BYTES)) {
213             IO.writeBytes(urlConnection.getOutputStream(), requestMessage.getBodyBytes());
214         }
215         urlConnection.getOutputStream().flush();
216     }
217 
218     protected StreamResponseMessage createResponse(HttpURLConnection urlConnection, InputStream inputStream) throws Exception {
219 
220         if (urlConnection.getResponseCode() == -1) {
221             log.fine("Did not receive valid HTTP response");
222             return null;
223         }
224 
225         // Status
226         UpnpResponse responseOperation = new UpnpResponse(urlConnection.getResponseCode(), urlConnection.getResponseMessage());
227 
228         log.fine("Received response: " + responseOperation);
229 
230         // Message
231         StreamResponseMessage responseMessage = new StreamResponseMessage(responseOperation);
232 
233         // Headers
234         responseMessage.setHeaders(new UpnpHeaders(urlConnection.getHeaderFields()));
235 
236         // Body
237         byte[] bodyBytes = null;
238         InputStream is = null;
239         try {
240             is = inputStream;
241             if (inputStream != null) bodyBytes = IO.readBytes(is);
242         } finally {
243             if (is != null)
244                 is.close();
245         }
246 
247         if (bodyBytes != null && bodyBytes.length > 0 && responseMessage.isContentTypeMissingOrText()) {
248 
249             log.fine("Response contains textual entity body, converting then setting string on message");
250             responseMessage.setBodyCharacters(bodyBytes);
251 
252         } else if (bodyBytes != null && bodyBytes.length > 0) {
253 
254             log.fine("Response contains binary entity body, setting bytes on message");
255             responseMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes);
256 
257         } else {
258             log.fine("Response did not contain entity body");
259         }
260 
261         log.fine("Response message complete: " + responseMessage);
262         return responseMessage;
263     }
264 
265     /**
266      * The SUNW morons restrict the JDK handlers to GET/POST/etc for "security" reasons.
267      * They do not understand HTTP. This is the hilarious comment in their source:
268      * <p/>
269      * "This restriction will prevent people from using this class to experiment w/ new
270      * HTTP methods using java.  But it should be placed for security - the request String
271      * could be arbitrarily long."
272      */
273     static class UpnpURLConnection extends sun.net.www.protocol.http.HttpURLConnection {
274 
275         private static final String[] methods = {
276                 "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE",
277                 "SUBSCRIBE", "UNSUBSCRIBE", "NOTIFY"
278         };
279 
280         protected UpnpURLConnection(URL u, Handler handler) throws IOException {
281             super(u, handler);
282         }
283 
284         public UpnpURLConnection(URL u, String host, int port) throws IOException {
285             super(u, host, port);
286         }
287 
288         public synchronized OutputStream getOutputStream() throws IOException {
289             OutputStream os;
290             String savedMethod = method;
291             // see if the method supports output
292             if (method.equals("PUT") || method.equals("POST") || method.equals("NOTIFY")) {
293                 // fake the method so the superclass method sets its instance variables
294                 method = "PUT";
295             } else {
296                 // use any method that doesn't support output, an exception will be
297                 // raised by the superclass
298                 method = "GET";
299             }
300             os = super.getOutputStream();
301             method = savedMethod;
302             return os;
303         }
304 
305         public void setRequestMethod(String method) throws ProtocolException {
306             if (connected) {
307                 throw new ProtocolException("Cannot reset method once connected");
308             }
309             for (String m : methods) {
310                 if (m.equals(method)) {
311                     this.method = method;
312                     return;
313                 }
314             }
315             throw new ProtocolException("Invalid UPnP HTTP method: " + method);
316         }
317     }
318 
319 }
320 
321