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
26package com.sun.xml.internal.ws.transport.http.client;
27
28import com.sun.istack.internal.NotNull;
29import com.sun.xml.internal.ws.api.SOAPVersion;
30import com.sun.xml.internal.ws.api.WSBinding;
31import com.sun.xml.internal.ws.api.ha.StickyFeature;
32import com.sun.xml.internal.ws.api.message.Packet;
33import com.sun.xml.internal.ws.api.pipe.*;
34import com.sun.xml.internal.ws.api.pipe.helper.AbstractTubeImpl;
35import com.sun.xml.internal.ws.client.ClientTransportException;
36import com.sun.xml.internal.ws.developer.HttpConfigFeature;
37import com.sun.xml.internal.ws.resources.ClientMessages;
38import com.sun.xml.internal.ws.resources.WsservletMessages;
39import com.sun.xml.internal.ws.transport.Headers;
40import com.sun.xml.internal.ws.transport.http.HttpAdapter;
41import com.sun.xml.internal.ws.util.ByteArrayBuffer;
42import com.sun.xml.internal.ws.util.RuntimeVersion;
43import com.sun.xml.internal.ws.util.StreamUtils;
44
45import javax.xml.bind.DatatypeConverter;
46import javax.xml.ws.BindingProvider;
47import javax.xml.ws.WebServiceException;
48import javax.xml.ws.WebServiceFeature;
49import javax.xml.ws.handler.MessageContext;
50import javax.xml.ws.soap.SOAPBinding;
51import java.io.*;
52import java.net.CookieHandler;
53import java.net.HttpURLConnection;
54import java.util.*;
55import java.util.Map.Entry;
56import java.util.logging.Level;
57import 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 */
67public 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}
458