1
2
3
4
5
6
7
8
9
10
11
12
13
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
82
83
84
85
86
87
88
89
90
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
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
106
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
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
190 urlConnection.disconnect();
191 }
192 }
193 }
194
195 @Override
196 public void stop() {
197
198 }
199
200 protected void applyRequestProperties(HttpURLConnection urlConnection, StreamRequestMessage requestMessage) {
201
202 urlConnection.setInstanceFollowRedirects(false);
203
204
205
206
207
208
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
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
259 UpnpResponse responseOperation = new UpnpResponse(urlConnection.getResponseCode(), urlConnection.getResponseMessage());
260
261 log.fine("Received response: " + responseOperation);
262
263
264 StreamResponseMessage responseMessage = new StreamResponseMessage(responseOperation);
265
266
267 responseMessage.setHeaders(new UpnpHeaders(urlConnection.getHeaderFields()));
268
269
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