|
1 /* |
|
2 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved. |
|
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
|
4 * |
|
5 * This code is free software; you can redistribute it and/or modify it |
|
6 * under the terms of the GNU General Public License version 2 only, as |
|
7 * published by the Free Software Foundation. Oracle designates this |
|
8 * particular file as subject to the "Classpath" exception as provided |
|
9 * by Oracle in the LICENSE file that accompanied this code. |
|
10 * |
|
11 * This code is distributed in the hope that it will be useful, but WITHOUT |
|
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
|
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
|
14 * version 2 for more details (a copy is included in the LICENSE file that |
|
15 * accompanied this code). |
|
16 * |
|
17 * You should have received a copy of the GNU General Public License version |
|
18 * 2 along with this work; if not, write to the Free Software Foundation, |
|
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
|
20 * |
|
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
|
22 * or visit www.oracle.com if you need additional information or have any |
|
23 * questions. |
|
24 */ |
|
25 |
|
26 package com.sun.xml.internal.ws.transport.http.client; |
|
27 |
|
28 import com.sun.istack.internal.NotNull; |
|
29 import com.sun.xml.internal.ws.api.SOAPVersion; |
|
30 import com.sun.xml.internal.ws.api.WSBinding; |
|
31 import com.sun.xml.internal.ws.api.ha.StickyFeature; |
|
32 import com.sun.xml.internal.ws.api.message.Packet; |
|
33 import com.sun.xml.internal.ws.api.pipe.*; |
|
34 import com.sun.xml.internal.ws.api.pipe.helper.AbstractTubeImpl; |
|
35 import com.sun.xml.internal.ws.client.ClientTransportException; |
|
36 import com.sun.xml.internal.ws.developer.HttpConfigFeature; |
|
37 import com.sun.xml.internal.ws.resources.ClientMessages; |
|
38 import com.sun.xml.internal.ws.resources.WsservletMessages; |
|
39 import com.sun.xml.internal.ws.transport.Headers; |
|
40 import com.sun.xml.internal.ws.transport.http.HttpAdapter; |
|
41 import com.sun.xml.internal.ws.util.ByteArrayBuffer; |
|
42 import com.sun.xml.internal.ws.util.RuntimeVersion; |
|
43 import com.sun.xml.internal.ws.util.StreamUtils; |
|
44 |
|
45 import javax.xml.bind.DatatypeConverter; |
|
46 import javax.xml.ws.BindingProvider; |
|
47 import javax.xml.ws.WebServiceException; |
|
48 import javax.xml.ws.WebServiceFeature; |
|
49 import javax.xml.ws.handler.MessageContext; |
|
50 import javax.xml.ws.soap.SOAPBinding; |
|
51 import java.io.*; |
|
52 import java.net.CookieHandler; |
|
53 import java.net.HttpURLConnection; |
|
54 import java.util.*; |
|
55 import java.util.Map.Entry; |
|
56 import java.util.logging.Level; |
|
57 import java.util.logging.Logger; |
|
58 |
|
59 /** |
|
60 * {@link Tube} that sends a request to a remote HTTP server. |
|
61 * |
|
62 * TODO: need to create separate HTTP transport pipes for binding. SOAP1.1, SOAP1.2, |
|
63 * TODO: XML/HTTP differ in handling status codes. |
|
64 * |
|
65 * @author Jitendra Kotamraju |
|
66 */ |
|
67 public class HttpTransportPipe extends AbstractTubeImpl { |
|
68 |
|
69 private static final List<String> USER_AGENT = Collections.singletonList(RuntimeVersion.VERSION.toString()); |
|
70 private static final Logger LOGGER = Logger.getLogger(HttpTransportPipe.class.getName()); |
|
71 |
|
72 /** |
|
73 * Dumps what goes across HTTP transport. |
|
74 */ |
|
75 public static boolean dump; |
|
76 |
|
77 private final Codec codec; |
|
78 private final WSBinding binding; |
|
79 private final CookieHandler cookieJar; // shared object among the tubes |
|
80 private final boolean sticky; |
|
81 |
|
82 static { |
|
83 boolean b; |
|
84 try { |
|
85 b = Boolean.getBoolean(HttpTransportPipe.class.getName()+".dump"); |
|
86 } catch( Throwable t ) { |
|
87 b = false; |
|
88 } |
|
89 dump = b; |
|
90 } |
|
91 |
|
92 public HttpTransportPipe(Codec codec, WSBinding binding) { |
|
93 this.codec = codec; |
|
94 this.binding = binding; |
|
95 this.sticky = isSticky(binding); |
|
96 HttpConfigFeature configFeature = binding.getFeature(HttpConfigFeature.class); |
|
97 if (configFeature == null) { |
|
98 configFeature = new HttpConfigFeature(); |
|
99 } |
|
100 this.cookieJar = configFeature.getCookieHandler(); |
|
101 } |
|
102 |
|
103 private static boolean isSticky(WSBinding binding) { |
|
104 boolean tSticky = false; |
|
105 WebServiceFeature[] features = binding.getFeatures().toArray(); |
|
106 for(WebServiceFeature f : features) { |
|
107 if (f instanceof StickyFeature) { |
|
108 tSticky = true; |
|
109 break; |
|
110 } |
|
111 } |
|
112 return tSticky; |
|
113 } |
|
114 |
|
115 /* |
|
116 * Copy constructor for {@link Tube#copy(TubeCloner)}. |
|
117 */ |
|
118 private HttpTransportPipe(HttpTransportPipe that, TubeCloner cloner) { |
|
119 this(that.codec.copy(), that.binding); |
|
120 cloner.add(that,this); |
|
121 } |
|
122 |
|
123 @Override |
|
124 public NextAction processException(@NotNull Throwable t) { |
|
125 return doThrow(t); |
|
126 } |
|
127 |
|
128 @Override |
|
129 public NextAction processRequest(@NotNull Packet request) { |
|
130 return doReturnWith(process(request)); |
|
131 } |
|
132 |
|
133 @Override |
|
134 public NextAction processResponse(@NotNull Packet response) { |
|
135 return doReturnWith(response); |
|
136 } |
|
137 |
|
138 protected HttpClientTransport getTransport(Packet request, Map<String, List<String>> reqHeaders) { |
|
139 return new HttpClientTransport(request, reqHeaders); |
|
140 } |
|
141 |
|
142 @Override |
|
143 public Packet process(Packet request) { |
|
144 HttpClientTransport con; |
|
145 try { |
|
146 // get transport headers from message |
|
147 Map<String, List<String>> reqHeaders = new Headers(); |
|
148 @SuppressWarnings("unchecked") |
|
149 Map<String, List<String>> userHeaders = (Map<String, List<String>>) request.invocationProperties.get(MessageContext.HTTP_REQUEST_HEADERS); |
|
150 boolean addUserAgent = true; |
|
151 if (userHeaders != null) { |
|
152 // userHeaders may not be modifiable like SingletonMap, just copy them |
|
153 reqHeaders.putAll(userHeaders); |
|
154 // application wants to use its own User-Agent header |
|
155 if (userHeaders.get("User-Agent") != null) { |
|
156 addUserAgent = false; |
|
157 } |
|
158 } |
|
159 if (addUserAgent) { |
|
160 reqHeaders.put("User-Agent", USER_AGENT); |
|
161 } |
|
162 |
|
163 addBasicAuth(request, reqHeaders); |
|
164 addCookies(request, reqHeaders); |
|
165 |
|
166 con = getTransport(request, reqHeaders); |
|
167 request.addSatellite(new HttpResponseProperties(con)); |
|
168 |
|
169 ContentType ct = codec.getStaticContentType(request); |
|
170 if (ct == null) { |
|
171 ByteArrayBuffer buf = new ByteArrayBuffer(); |
|
172 |
|
173 ct = codec.encode(request, buf); |
|
174 // data size is available, set it as Content-Length |
|
175 reqHeaders.put("Content-Length", Collections.singletonList(Integer.toString(buf.size()))); |
|
176 reqHeaders.put("Content-Type", Collections.singletonList(ct.getContentType())); |
|
177 if (ct.getAcceptHeader() != null) { |
|
178 reqHeaders.put("Accept", Collections.singletonList(ct.getAcceptHeader())); |
|
179 } |
|
180 if (binding instanceof SOAPBinding) { |
|
181 writeSOAPAction(reqHeaders, ct.getSOAPActionHeader()); |
|
182 } |
|
183 |
|
184 if (dump || LOGGER.isLoggable(Level.FINER)) { |
|
185 dump(buf, "HTTP request", reqHeaders); |
|
186 } |
|
187 |
|
188 buf.writeTo(con.getOutput()); |
|
189 } else { |
|
190 // Set static Content-Type |
|
191 reqHeaders.put("Content-Type", Collections.singletonList(ct.getContentType())); |
|
192 if (ct.getAcceptHeader() != null) { |
|
193 reqHeaders.put("Accept", Collections.singletonList(ct.getAcceptHeader())); |
|
194 } |
|
195 if (binding instanceof SOAPBinding) { |
|
196 writeSOAPAction(reqHeaders, ct.getSOAPActionHeader()); |
|
197 } |
|
198 |
|
199 if(dump || LOGGER.isLoggable(Level.FINER)) { |
|
200 ByteArrayBuffer buf = new ByteArrayBuffer(); |
|
201 codec.encode(request, buf); |
|
202 dump(buf, "HTTP request - "+request.endpointAddress, reqHeaders); |
|
203 OutputStream out = con.getOutput(); |
|
204 if (out != null) { |
|
205 buf.writeTo(out); |
|
206 } |
|
207 } else { |
|
208 OutputStream os = con.getOutput(); |
|
209 if (os != null) { |
|
210 codec.encode(request, os); |
|
211 } |
|
212 } |
|
213 } |
|
214 |
|
215 con.closeOutput(); |
|
216 |
|
217 return createResponsePacket(request, con); |
|
218 } catch(WebServiceException wex) { |
|
219 throw wex; |
|
220 } catch(Exception ex) { |
|
221 throw new WebServiceException(ex); |
|
222 } |
|
223 } |
|
224 |
|
225 private Packet createResponsePacket(Packet request, HttpClientTransport con) throws IOException { |
|
226 con.readResponseCodeAndMessage(); // throws IOE |
|
227 recordCookies(request, con); |
|
228 |
|
229 InputStream responseStream = con.getInput(); |
|
230 if (dump || LOGGER.isLoggable(Level.FINER)) { |
|
231 ByteArrayBuffer buf = new ByteArrayBuffer(); |
|
232 if (responseStream != null) { |
|
233 buf.write(responseStream); |
|
234 responseStream.close(); |
|
235 } |
|
236 dump(buf,"HTTP response - "+request.endpointAddress+" - "+con.statusCode, con.getHeaders()); |
|
237 responseStream = buf.newInputStream(); |
|
238 } |
|
239 |
|
240 // Check if stream contains any data |
|
241 int cl = con.contentLength; |
|
242 InputStream tempIn = null; |
|
243 if (cl == -1) { // No Content-Length header |
|
244 tempIn = StreamUtils.hasSomeData(responseStream); |
|
245 if (tempIn != null) { |
|
246 responseStream = tempIn; |
|
247 } |
|
248 } |
|
249 if (cl == 0 || (cl == -1 && tempIn == null)) { |
|
250 if(responseStream != null) { |
|
251 responseStream.close(); // No data, so close the stream |
|
252 responseStream = null; |
|
253 } |
|
254 |
|
255 } |
|
256 |
|
257 // Allows only certain http status codes for a binding. For all |
|
258 // other status codes, throws exception |
|
259 checkStatusCode(responseStream, con); // throws ClientTransportException |
|
260 |
|
261 Packet reply = request.createClientResponse(null); |
|
262 reply.wasTransportSecure = con.isSecure(); |
|
263 if (responseStream != null) { |
|
264 String contentType = con.getContentType(); |
|
265 if (contentType != null && contentType.contains("text/html") && binding instanceof SOAPBinding) { |
|
266 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(con.statusCode, con.statusMessage)); |
|
267 } |
|
268 codec.decode(responseStream, contentType, reply); |
|
269 } |
|
270 return reply; |
|
271 } |
|
272 |
|
273 /* |
|
274 * Allows the following HTTP status codes. |
|
275 * SOAP 1.1/HTTP - 200, 202, 500 |
|
276 * SOAP 1.2/HTTP - 200, 202, 400, 500 |
|
277 * XML/HTTP - all |
|
278 * |
|
279 * For all other status codes, it throws an exception |
|
280 */ |
|
281 private void checkStatusCode(InputStream in, HttpClientTransport con) throws IOException { |
|
282 int statusCode = con.statusCode; |
|
283 String statusMessage = con.statusMessage; |
|
284 // SOAP1.1 and SOAP1.2 differ here |
|
285 if (binding instanceof SOAPBinding) { |
|
286 if (binding.getSOAPVersion() == SOAPVersion.SOAP_12) { |
|
287 //In SOAP 1.2, Fault messages can be sent with 4xx and 5xx error codes |
|
288 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || isErrorCode(statusCode)) { |
|
289 // acceptable status codes for SOAP 1.2 |
|
290 if (isErrorCode(statusCode) && in == null) { |
|
291 // No envelope for the error, so throw an exception with http error details |
|
292 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); |
|
293 } |
|
294 return; |
|
295 } |
|
296 } else { |
|
297 // SOAP 1.1 |
|
298 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) { |
|
299 // acceptable status codes for SOAP 1.1 |
|
300 if (statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR && in == null) { |
|
301 // No envelope for the error, so throw an exception with http error details |
|
302 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); |
|
303 } |
|
304 return; |
|
305 } |
|
306 } |
|
307 if (in != null) { |
|
308 in.close(); |
|
309 } |
|
310 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); |
|
311 } |
|
312 // Every status code is OK for XML/HTTP |
|
313 } |
|
314 |
|
315 private boolean isErrorCode(int code) { |
|
316 //if(code/100 == 5/*Server-side error*/ || code/100 == 4 /*client error*/ ) { |
|
317 return code == 500 || code == 400; |
|
318 } |
|
319 |
|
320 private void addCookies(Packet context, Map<String, List<String>> reqHeaders) throws IOException { |
|
321 Boolean shouldMaintainSessionProperty = |
|
322 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY); |
|
323 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) { |
|
324 return; // explicitly turned off |
|
325 } |
|
326 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) { |
|
327 Map<String, List<String>> rememberedCookies = cookieJar.get(context.endpointAddress.getURI(), reqHeaders); |
|
328 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie"); |
|
329 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie2"); |
|
330 } |
|
331 } |
|
332 |
|
333 private void processCookieHeaders(Map<String, List<String>> requestHeaders, Map<String, List<String>> rememberedCookies, String cookieHeader) { |
|
334 List<String> jarCookies = rememberedCookies.get(cookieHeader); |
|
335 if (jarCookies != null && !jarCookies.isEmpty()) { |
|
336 List<String> resultCookies = mergeUserCookies(jarCookies, requestHeaders.get(cookieHeader)); |
|
337 requestHeaders.put(cookieHeader, resultCookies); |
|
338 } |
|
339 } |
|
340 |
|
341 private List<String> mergeUserCookies(List<String> rememberedCookies, List<String> userCookies) { |
|
342 |
|
343 // nothing to merge |
|
344 if (userCookies == null || userCookies.isEmpty()) { |
|
345 return rememberedCookies; |
|
346 } |
|
347 |
|
348 Map<String, String> map = new HashMap<String, String>(); |
|
349 cookieListToMap(rememberedCookies, map); |
|
350 cookieListToMap(userCookies, map); |
|
351 |
|
352 return new ArrayList<String>(map.values()); |
|
353 } |
|
354 |
|
355 private void cookieListToMap(List<String> cookieList, Map<String, String> targetMap) { |
|
356 for(String cookie : cookieList) { |
|
357 int index = cookie.indexOf("="); |
|
358 String cookieName = cookie.substring(0, index); |
|
359 targetMap.put(cookieName, cookie); |
|
360 } |
|
361 } |
|
362 |
|
363 private void recordCookies(Packet context, HttpClientTransport con) throws IOException { |
|
364 Boolean shouldMaintainSessionProperty = |
|
365 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY); |
|
366 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) { |
|
367 return; // explicitly turned off |
|
368 } |
|
369 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) { |
|
370 cookieJar.put(context.endpointAddress.getURI(), con.getHeaders()); |
|
371 } |
|
372 } |
|
373 |
|
374 private void addBasicAuth(Packet context, Map<String, List<String>> reqHeaders) { |
|
375 String user = (String) context.invocationProperties.get(BindingProvider.USERNAME_PROPERTY); |
|
376 if (user != null) { |
|
377 String pw = (String) context.invocationProperties.get(BindingProvider.PASSWORD_PROPERTY); |
|
378 if (pw != null) { |
|
379 StringBuilder buf = new StringBuilder(user); |
|
380 buf.append(":"); |
|
381 buf.append(pw); |
|
382 String creds = DatatypeConverter.printBase64Binary(buf.toString().getBytes()); |
|
383 reqHeaders.put("Authorization", Collections.singletonList("Basic "+creds)); |
|
384 } |
|
385 } |
|
386 } |
|
387 |
|
388 /* |
|
389 * write SOAPAction header if the soapAction parameter is non-null or BindingProvider properties set. |
|
390 * BindingProvider properties take precedence. |
|
391 */ |
|
392 private void writeSOAPAction(Map<String, List<String>> reqHeaders, String soapAction) { |
|
393 //dont write SOAPAction HTTP header for SOAP 1.2 messages. |
|
394 if(SOAPVersion.SOAP_12.equals(binding.getSOAPVersion())) { |
|
395 return; |
|
396 } |
|
397 if (soapAction != null) { |
|
398 reqHeaders.put("SOAPAction", Collections.singletonList(soapAction)); |
|
399 } else { |
|
400 reqHeaders.put("SOAPAction", Collections.singletonList("\"\"")); |
|
401 } |
|
402 } |
|
403 |
|
404 @Override |
|
405 public void preDestroy() { |
|
406 // nothing to do. Intentionally left empty. |
|
407 } |
|
408 |
|
409 @Override |
|
410 public HttpTransportPipe copy(TubeCloner cloner) { |
|
411 return new HttpTransportPipe(this,cloner); |
|
412 } |
|
413 |
|
414 |
|
415 private void dump(ByteArrayBuffer buf, String caption, Map<String, List<String>> headers) throws IOException { |
|
416 ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
|
417 PrintWriter pw = new PrintWriter(baos, true); |
|
418 pw.println("---["+caption +"]---"); |
|
419 for (Entry<String,List<String>> header : headers.entrySet()) { |
|
420 if(header.getValue().isEmpty()) { |
|
421 // I don't think this is legal, but let's just dump it, |
|
422 // as the point of the dump is to uncover problems. |
|
423 pw.println(header.getValue()); |
|
424 } else { |
|
425 for (String value : header.getValue()) { |
|
426 pw.println(header.getKey()+": "+value); |
|
427 } |
|
428 } |
|
429 } |
|
430 |
|
431 if (buf.size() > HttpAdapter.dump_threshold) { |
|
432 byte[] b = buf.getRawData(); |
|
433 baos.write(b, 0, HttpAdapter.dump_threshold); |
|
434 pw.println(); |
|
435 pw.println(WsservletMessages.MESSAGE_TOO_LONG(HttpAdapter.class.getName() + ".dumpTreshold")); |
|
436 } else { |
|
437 buf.writeTo(baos); |
|
438 } |
|
439 pw.println("--------------------"); |
|
440 |
|
441 String msg = baos.toString(); |
|
442 if (dump) { |
|
443 System.out.println(msg); |
|
444 } |
|
445 if (LOGGER.isLoggable(Level.FINER)) { |
|
446 LOGGER.log(Level.FINER, msg); |
|
447 } |
|
448 } |
|
449 |
|
450 } |