1 /*
   2  * Copyright (c) 2015, 2018, 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 jdk.internal.net.http;
  27 
  28 import java.io.IOException;
  29 import java.net.ConnectException;
  30 import java.net.http.HttpConnectTimeoutException;
  31 import java.util.Iterator;
  32 import java.util.LinkedList;
  33 import java.security.AccessControlContext;
  34 import java.util.concurrent.CompletableFuture;
  35 import java.util.concurrent.CompletionException;
  36 import java.util.concurrent.ExecutionException;
  37 import java.util.concurrent.Executor;
  38 import java.util.concurrent.atomic.AtomicInteger;
  39 import java.util.function.Function;
  40 
  41 import java.net.http.HttpClient;
  42 import java.net.http.HttpRequest;
  43 import java.net.http.HttpResponse;
  44 import java.net.http.HttpResponse.PushPromiseHandler;
  45 import java.net.http.HttpTimeoutException;
  46 import jdk.internal.net.http.common.Log;
  47 import jdk.internal.net.http.common.Logger;
  48 import jdk.internal.net.http.common.MinimalFuture;
  49 import jdk.internal.net.http.common.ConnectionExpiredException;
  50 import jdk.internal.net.http.common.Utils;
  51 import static jdk.internal.net.http.common.MinimalFuture.completedFuture;
  52 import static jdk.internal.net.http.common.MinimalFuture.failedFuture;
  53 
  54 /**
  55  * Encapsulates multiple Exchanges belonging to one HttpRequestImpl.
  56  * - manages filters
  57  * - retries due to filters.
  58  * - I/O errors and most other exceptions get returned directly to user
  59  *
  60  * Creates a new Exchange for each request/response interaction
  61  */
  62 class MultiExchange<T> {
  63 
  64     static final Logger debug =
  65             Utils.getDebugLogger("MultiExchange"::toString, Utils.DEBUG);
  66 
  67     private final HttpRequest userRequest; // the user request
  68     private final HttpRequestImpl request; // a copy of the user request
  69     final AccessControlContext acc;
  70     final HttpClientImpl client;
  71     final HttpResponse.BodyHandler<T> responseHandler;
  72     final HttpClientImpl.DelegatingExecutor executor;
  73     final AtomicInteger attempts = new AtomicInteger();
  74     HttpRequestImpl currentreq; // used for retries & redirect
  75     HttpRequestImpl previousreq; // used for retries & redirect
  76     Exchange<T> exchange; // the current exchange
  77     Exchange<T> previous;
  78     volatile Throwable retryCause;
  79     volatile boolean expiredOnce;
  80     volatile HttpResponse<T> response = null;
  81 
  82     // Maximum number of times a request will be retried/redirected
  83     // for any reason
  84 
  85     static final int DEFAULT_MAX_ATTEMPTS = 5;
  86     static final int max_attempts = Utils.getIntegerNetProperty(
  87             "jdk.httpclient.redirects.retrylimit", DEFAULT_MAX_ATTEMPTS
  88     );
  89 
  90     private final LinkedList<HeaderFilter> filters;
  91     ResponseTimerEvent responseTimerEvent;
  92     volatile boolean cancelled;
  93     final PushGroup<T> pushGroup;
  94 
  95     /**
  96      * Filter fields. These are attached as required by filters
  97      * and only used by the filter implementations. This could be
  98      * generalised into Objects that are passed explicitly to the filters
  99      * (one per MultiExchange object, and one per Exchange object possibly)
 100      */
 101     volatile AuthenticationFilter.AuthInfo serverauth, proxyauth;
 102     // RedirectHandler
 103     volatile int numberOfRedirects = 0;
 104 
 105     /**
 106      * MultiExchange with one final response.
 107      */
 108     MultiExchange(HttpRequest userRequest,
 109                   HttpRequestImpl requestImpl,
 110                   HttpClientImpl client,
 111                   HttpResponse.BodyHandler<T> responseHandler,
 112                   PushPromiseHandler<T> pushPromiseHandler,
 113                   AccessControlContext acc) {
 114         this.previous = null;
 115         this.userRequest = userRequest;
 116         this.request = requestImpl;
 117         this.currentreq = request;
 118         this.previousreq = null;
 119         this.client = client;
 120         this.filters = client.filterChain();
 121         this.acc = acc;
 122         this.executor = client.theExecutor();
 123         this.responseHandler = responseHandler;
 124 
 125         if (pushPromiseHandler != null) {
 126             Executor executor = acc == null
 127                     ? this.executor.delegate()
 128                     : new PrivilegedExecutor(this.executor.delegate(), acc);
 129             this.pushGroup = new PushGroup<>(pushPromiseHandler, request, executor);
 130         } else {
 131             pushGroup = null;
 132         }
 133 
 134         this.exchange = new Exchange<>(request, this);
 135     }
 136 
 137     synchronized Exchange<T> getExchange() {
 138         return exchange;
 139     }
 140 
 141     HttpClientImpl client() {
 142         return client;
 143     }
 144 
 145     HttpClient.Version version() {
 146         HttpClient.Version vers = request.version().orElse(client.version());
 147         if (vers == HttpClient.Version.HTTP_2 && !request.secure() && request.proxy() != null)
 148             vers = HttpClient.Version.HTTP_1_1;
 149         return vers;
 150     }
 151 
 152     private synchronized void setExchange(Exchange<T> exchange) {
 153         if (this.exchange != null && exchange != this.exchange) {
 154             this.exchange.released();
 155         }
 156         this.exchange = exchange;
 157     }
 158 
 159     private void cancelTimer() {
 160         if (responseTimerEvent != null) {
 161             client.cancelTimer(responseTimerEvent);
 162         }
 163     }
 164 
 165     private void requestFilters(HttpRequestImpl r) throws IOException {
 166         Log.logTrace("Applying request filters");
 167         for (HeaderFilter filter : filters) {
 168             Log.logTrace("Applying {0}", filter);
 169             filter.request(r, this);
 170         }
 171         Log.logTrace("All filters applied");
 172     }
 173 
 174     private HttpRequestImpl responseFilters(Response response) throws IOException
 175     {
 176         Log.logTrace("Applying response filters");
 177         Iterator<HeaderFilter> reverseItr = filters.descendingIterator();
 178         while (reverseItr.hasNext()) {
 179             HeaderFilter filter = reverseItr.next();
 180             Log.logTrace("Applying {0}", filter);
 181             HttpRequestImpl newreq = filter.response(response);
 182             if (newreq != null) {
 183                 Log.logTrace("New request: stopping filters");
 184                 return newreq;
 185             }
 186         }
 187         Log.logTrace("All filters applied");
 188         return null;
 189     }
 190 
 191     public void cancel(IOException cause) {
 192         cancelled = true;
 193         getExchange().cancel(cause);
 194     }
 195 
 196     public CompletableFuture<HttpResponse<T>> responseAsync(Executor executor) {
 197         CompletableFuture<Void> start = new MinimalFuture<>();
 198         CompletableFuture<HttpResponse<T>> cf = responseAsync0(start);
 199         start.completeAsync( () -> null, executor); // trigger execution
 200         return cf;
 201     }
 202 
 203     private CompletableFuture<HttpResponse<T>>
 204     responseAsync0(CompletableFuture<Void> start) {
 205         return start.thenCompose( v -> responseAsyncImpl())
 206                     .thenCompose((Response r) -> {
 207                         Exchange<T> exch = getExchange();
 208                         return exch.readBodyAsync(responseHandler)
 209                             .thenApply((T body) -> {
 210                                 this.response =
 211                                     new HttpResponseImpl<>(r.request(), r, this.response, body, exch);
 212                                 return this.response;
 213                             });
 214                     });
 215     }
 216 
 217     private CompletableFuture<Response> responseAsyncImpl() {
 218         CompletableFuture<Response> cf;
 219         if (attempts.incrementAndGet() > max_attempts) {
 220             cf = failedFuture(new IOException("Too many retries", retryCause));
 221         } else {
 222             if (currentreq.timeout().isPresent()) {
 223                 responseTimerEvent = ResponseTimerEvent.of(this);
 224                 client.registerTimer(responseTimerEvent);
 225             }
 226             try {
 227                 // 1. apply request filters
 228                 // if currentreq == previousreq the filters have already
 229                 // been applied once. Applying them a second time might
 230                 // cause some headers values to be added twice: for
 231                 // instance, the same cookie might be added again.
 232                 if (currentreq != previousreq) {
 233                     requestFilters(currentreq);
 234                 }
 235             } catch (IOException e) {
 236                 return failedFuture(e);
 237             }
 238             Exchange<T> exch = getExchange();
 239             // 2. get response
 240             cf = exch.responseAsync()
 241                      .thenCompose((Response response) -> {
 242                         HttpRequestImpl newrequest;
 243                         try {
 244                             // 3. apply response filters
 245                             newrequest = responseFilters(response);
 246                         } catch (IOException e) {
 247                             return failedFuture(e);
 248                         }
 249                         // 4. check filter result and repeat or continue
 250                         if (newrequest == null) {
 251                             if (attempts.get() > 1) {
 252                                 Log.logError("Succeeded on attempt: " + attempts);
 253                             }
 254                             return completedFuture(response);
 255                         } else {
 256                             this.response =
 257                                 new HttpResponseImpl<>(currentreq, response, this.response, null, exch);
 258                             Exchange<T> oldExch = exch;
 259                             return exch.ignoreBody().handle((r,t) -> {
 260                                 previousreq = currentreq;
 261                                 currentreq = newrequest;
 262                                 expiredOnce = false;
 263                                 setExchange(new Exchange<>(currentreq, this, acc));
 264                                 return responseAsyncImpl();
 265                             }).thenCompose(Function.identity());
 266                         } })
 267                      .handle((response, ex) -> {
 268                         // 5. handle errors and cancel any timer set
 269                         cancelTimer();
 270                         if (ex == null) {
 271                             assert response != null;
 272                             return completedFuture(response);
 273                         }
 274                         // all exceptions thrown are handled here
 275                         CompletableFuture<Response> errorCF = getExceptionalCF(ex);
 276                         if (errorCF == null) {
 277                             return responseAsyncImpl();
 278                         } else {
 279                             return errorCF;
 280                         } })
 281                      .thenCompose(Function.identity());
 282         }
 283         return cf;
 284     }
 285 
 286     private static boolean retryPostValue() {
 287         String s = Utils.getNetProperty("jdk.httpclient.enableAllMethodRetry");
 288         if (s == null)
 289             return false;
 290         return s.isEmpty() ? true : Boolean.parseBoolean(s);
 291     }
 292 
 293     private static boolean retryConnect() {
 294         String s = Utils.getNetProperty("jdk.httpclient.disableRetryConnect");
 295         if (s == null)
 296             return false;
 297         return s.isEmpty() ? true : Boolean.parseBoolean(s);
 298     }
 299 
 300     /** True if ALL ( even non-idempotent ) requests can be automatic retried. */
 301     private static final boolean RETRY_ALWAYS = retryPostValue();
 302     /** True if ConnectException should cause a retry. Enabled by default */
 303     private static final boolean RETRY_CONNECT = retryConnect();
 304 
 305     /** Returns true is given request has an idempotent method. */
 306     private static boolean isIdempotentRequest(HttpRequest request) {
 307         String method = request.method();
 308         switch (method) {
 309             case "GET" :
 310             case "HEAD" :
 311                 return true;
 312             default :
 313                 return false;
 314         }
 315     }
 316 
 317     /** Returns true if the given request can be automatically retried. */
 318     private static boolean canRetryRequest(HttpRequest request) {
 319         if (RETRY_ALWAYS)
 320             return true;
 321         if (isIdempotentRequest(request))
 322             return true;
 323         return false;
 324     }
 325 
 326     private boolean retryOnFailure(Throwable t) {
 327         return t instanceof ConnectionExpiredException
 328                 || (RETRY_CONNECT && (t instanceof ConnectException));
 329     }
 330 
 331     private Throwable retryCause(Throwable t) {
 332         Throwable cause = t instanceof ConnectionExpiredException ? t.getCause() : t;
 333         return cause == null ? t : cause;
 334     }
 335 
 336     /**
 337      * Takes a Throwable and returns a suitable CompletableFuture that is
 338      * completed exceptionally, or null.
 339      */
 340     private CompletableFuture<Response> getExceptionalCF(Throwable t) {
 341         if ((t instanceof CompletionException) || (t instanceof ExecutionException)) {
 342             if (t.getCause() != null) {
 343                 t = t.getCause();
 344             }
 345         }
 346         if (cancelled && t instanceof IOException) {
 347             if (!(t instanceof HttpTimeoutException)) {
 348                 t = toTimeoutException((IOException)t);
 349             }
 350         } else if (retryOnFailure(t)) {
 351             Throwable cause = retryCause(t);
 352 
 353             if (!(t instanceof ConnectException)) {
 354                 if (!canRetryRequest(currentreq)) {
 355                     return failedFuture(cause); // fails with original cause
 356                 }
 357             }
 358 
 359             // allow the retry mechanism to do its work
 360             retryCause = cause;
 361             if (!expiredOnce) {
 362                 if (debug.on())
 363                     debug.log(t.getClass().getSimpleName() + " (async): retrying...", t);
 364                 expiredOnce = true;
 365                 // The connection was abruptly closed.
 366                 // We return null to retry the same request a second time.
 367                 // The request filters have already been applied to the
 368                 // currentreq, so we set previousreq = currentreq to
 369                 // prevent them from being applied again.
 370                 previousreq = currentreq;
 371                 return null;
 372             } else {
 373                 if (debug.on()) {
 374                     debug.log(t.getClass().getSimpleName()
 375                             + " (async): already retried once.", t);
 376                 }
 377                 t = cause;
 378             }
 379         }
 380         return failedFuture(t);
 381     }
 382 
 383     private HttpTimeoutException toTimeoutException(IOException ioe) {
 384         HttpTimeoutException t = null;
 385 
 386         // more specific, "request timed out", when connected
 387         Exchange<?> exchange = getExchange();
 388         if (exchange != null) {
 389             ExchangeImpl<?> exchangeImpl = exchange.exchImpl;
 390             if (exchangeImpl != null) {
 391                 if (exchangeImpl.connection().connected()) {
 392                     t = new HttpTimeoutException("request timed out");
 393                     t.initCause(ioe);  // TODO: remove this if all testing goes well
 394                 }
 395             }
 396         }
 397         if (t == null) {
 398             t = new HttpConnectTimeoutException("HTTP connect timed out");
 399             t.initCause(new ConnectException("HTTP connect timed out"));
 400         }
 401         return t;
 402     }
 403 }