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 }