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 }