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 }