1 /* 2 * Copyright (c) 1997, 2014, 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 //To avoid zero-length chunk for One-Way 261 if (cl ==-1 && con.statusCode == 202 && "Accepted".equals(con.statusMessage) && responseStream != null) { 262 ByteArrayBuffer buf = new ByteArrayBuffer(); 263 buf.write(responseStream); //What is within the responseStream? 264 responseStream.close(); 265 responseStream = (buf.size()==0)? null : buf.newInputStream(); 266 buf.close(); 267 } 268 Packet reply = request.createClientResponse(null); 269 reply.wasTransportSecure = con.isSecure(); 270 if (responseStream != null) { 271 String contentType = con.getContentType(); 272 if (contentType != null && contentType.contains("text/html") && binding instanceof SOAPBinding) { 273 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(con.statusCode, con.statusMessage)); 274 } 275 codec.decode(responseStream, contentType, reply); 276 } 277 return reply; 278 } 279 280 /* 281 * Allows the following HTTP status codes. 282 * SOAP 1.1/HTTP - 200, 202, 500 283 * SOAP 1.2/HTTP - 200, 202, 400, 500 284 * XML/HTTP - all 285 * 286 * For all other status codes, it throws an exception 287 */ 288 private void checkStatusCode(InputStream in, HttpClientTransport con) throws IOException { 289 int statusCode = con.statusCode; 290 String statusMessage = con.statusMessage; 291 // SOAP1.1 and SOAP1.2 differ here 292 if (binding instanceof SOAPBinding) { 293 if (binding.getSOAPVersion() == SOAPVersion.SOAP_12) { 294 //In SOAP 1.2, Fault messages can be sent with 4xx and 5xx error codes 295 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || isErrorCode(statusCode)) { 296 // acceptable status codes for SOAP 1.2 297 if (isErrorCode(statusCode) && in == null) { 298 // No envelope for the error, so throw an exception with http error details 299 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); 300 } 301 return; 302 } 303 } else { 304 // SOAP 1.1 305 if (statusCode == HttpURLConnection.HTTP_OK || statusCode == HttpURLConnection.HTTP_ACCEPTED || statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR) { 306 // acceptable status codes for SOAP 1.1 307 if (statusCode == HttpURLConnection.HTTP_INTERNAL_ERROR && in == null) { 308 // No envelope for the error, so throw an exception with http error details 309 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); 310 } 311 return; 312 } 313 } 314 if (in != null) { 315 in.close(); 316 } 317 throw new ClientTransportException(ClientMessages.localizableHTTP_STATUS_CODE(statusCode, statusMessage)); 318 } 319 // Every status code is OK for XML/HTTP 320 } 321 322 private boolean isErrorCode(int code) { 323 //if(code/100 == 5/*Server-side error*/ || code/100 == 4 /*client error*/ ) { 324 return code == 500 || code == 400; 325 } 326 327 private void addCookies(Packet context, Map<String, List<String>> reqHeaders) throws IOException { 328 Boolean shouldMaintainSessionProperty = 329 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY); 330 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) { 331 return; // explicitly turned off 332 } 333 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) { 334 Map<String, List<String>> rememberedCookies = cookieJar.get(context.endpointAddress.getURI(), reqHeaders); 335 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie"); 336 processCookieHeaders(reqHeaders, rememberedCookies, "Cookie2"); 337 } 338 } 339 340 private void processCookieHeaders(Map<String, List<String>> requestHeaders, Map<String, List<String>> rememberedCookies, String cookieHeader) { 341 List<String> jarCookies = rememberedCookies.get(cookieHeader); 342 if (jarCookies != null && !jarCookies.isEmpty()) { 343 List<String> resultCookies = mergeUserCookies(jarCookies, requestHeaders.get(cookieHeader)); 344 requestHeaders.put(cookieHeader, resultCookies); 345 } 346 } 347 348 private List<String> mergeUserCookies(List<String> rememberedCookies, List<String> userCookies) { 349 350 // nothing to merge 351 if (userCookies == null || userCookies.isEmpty()) { 352 return rememberedCookies; 353 } 354 355 Map<String, String> map = new HashMap<String, String>(); 356 cookieListToMap(rememberedCookies, map); 357 cookieListToMap(userCookies, map); 358 359 return new ArrayList<String>(map.values()); 360 } 361 362 private void cookieListToMap(List<String> cookieList, Map<String, String> targetMap) { 363 for(String cookie : cookieList) { 364 int index = cookie.indexOf("="); 365 String cookieName = cookie.substring(0, index); 366 targetMap.put(cookieName, cookie); 367 } 368 } 369 370 private void recordCookies(Packet context, HttpClientTransport con) throws IOException { 371 Boolean shouldMaintainSessionProperty = 372 (Boolean) context.invocationProperties.get(BindingProvider.SESSION_MAINTAIN_PROPERTY); 373 if (shouldMaintainSessionProperty != null && !shouldMaintainSessionProperty) { 374 return; // explicitly turned off 375 } 376 if (sticky || (shouldMaintainSessionProperty != null && shouldMaintainSessionProperty)) { 377 cookieJar.put(context.endpointAddress.getURI(), con.getHeaders()); 378 } 379 } 380 381 private void addBasicAuth(Packet context, Map<String, List<String>> reqHeaders) { 382 String user = (String) context.invocationProperties.get(BindingProvider.USERNAME_PROPERTY); 383 if (user != null) { 384 String pw = (String) context.invocationProperties.get(BindingProvider.PASSWORD_PROPERTY); 385 if (pw != null) { 386 StringBuilder buf = new StringBuilder(user); 387 buf.append(":"); 388 buf.append(pw); 389 String creds = DatatypeConverter.printBase64Binary(buf.toString().getBytes()); 390 reqHeaders.put("Authorization", Collections.singletonList("Basic "+creds)); 391 } 392 } 393 } 394 395 /* 396 * write SOAPAction header if the soapAction parameter is non-null or BindingProvider properties set. 397 * BindingProvider properties take precedence. 398 */ 399 private void writeSOAPAction(Map<String, List<String>> reqHeaders, String soapAction) { 400 //dont write SOAPAction HTTP header for SOAP 1.2 messages. 401 if(SOAPVersion.SOAP_12.equals(binding.getSOAPVersion())) { 402 return; 403 } 404 if (soapAction != null) { 405 reqHeaders.put("SOAPAction", Collections.singletonList(soapAction)); 406 } else { 407 reqHeaders.put("SOAPAction", Collections.singletonList("\"\"")); 408 } 409 } 410 411 @Override 412 public void preDestroy() { 413 // nothing to do. Intentionally left empty. 414 } 415 416 @Override 417 public HttpTransportPipe copy(TubeCloner cloner) { 418 return new HttpTransportPipe(this,cloner); 419 } 420 421 422 private void dump(ByteArrayBuffer buf, String caption, Map<String, List<String>> headers) throws IOException { 423 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 424 PrintWriter pw = new PrintWriter(baos, true); 425 pw.println("---["+caption +"]---"); 426 for (Entry<String,List<String>> header : headers.entrySet()) { 427 if(header.getValue().isEmpty()) { 428 // I don't think this is legal, but let's just dump it, 429 // as the point of the dump is to uncover problems. 430 pw.println(header.getValue()); 431 } else { 432 for (String value : header.getValue()) { 433 pw.println(header.getKey()+": "+value); 434 } 435 } 436 } 437 438 if (buf.size() > HttpAdapter.dump_threshold) { 439 byte[] b = buf.getRawData(); 440 baos.write(b, 0, HttpAdapter.dump_threshold); 441 pw.println(); 442 pw.println(WsservletMessages.MESSAGE_TOO_LONG(HttpAdapter.class.getName() + ".dumpTreshold")); 443 } else { 444 buf.writeTo(baos); 445 } 446 pw.println("--------------------"); 447 448 String msg = baos.toString(); 449 if (dump) { 450 System.out.println(msg); 451 } 452 if (LOGGER.isLoggable(Level.FINER)) { 453 LOGGER.log(Level.FINER, msg); 454 } 455 } 456 457 }