001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.client.jetty;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.nio.channels.*;
023import java.util.concurrent.*;
024
025import org.apache.juneau.rest.client.*;
026import org.eclipse.jetty.client.*;
027import org.eclipse.jetty.util.*;
028
029/**
030 * {@link HttpTransport} implementation backed by Jetty 12 {@link HttpClient}.
031 *
032 * <p>
033 * This transport is auto-discovered via {@link java.util.ServiceLoader} when
034 * {@code org.eclipse.jetty:jetty-client} is on the classpath.  You can also instantiate it explicitly:
035 *
036 * <p class='bjava'>
037 *    <jv>transport</jv> = JettyHttpTransport.<jsm>builder</jsm>()
038 *       .httpClient(<jk>new</jk> HttpClient())
039 *       .build();
040 *
041 *    <jv>client</jv> = RestClient.<jsm>builder</jsm>()
042 *       .transport(<jv>transport</jv>)
043 *       .build();
044 * </p>
045 *
046 * <p>
047 * The Jetty {@link HttpClient} is started automatically when the transport is created and stopped when
048 * {@link #close()} is called.  The request body is streamed via {@link OutputStreamRequestContent} using
049 * a background thread, avoiding full in-memory buffering.  The response header timeout defaults to 30 seconds
050 * and is configurable via {@link JettyHttpTransportBuilder#responseTimeoutMs(long)}.
051 *
052 * <h5 class='section'>See Also:</h5><ul>
053 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/NextGenRestClient">Next-Generation REST Client</a>
054 * </ul>
055 *
056 * @since 9.2.1
057 */
058@SuppressWarnings({
059   "resource" // Transport methods return Closeables owned by the caller; Eclipse JDT @Owning warning is by design.
060})
061public final class JettyHttpTransport implements HttpTransport {
062
063   private final HttpClient httpClient;
064   private final long responseTimeoutMs;
065   private final boolean policySupported;
066
067   JettyHttpTransport(JettyHttpTransportBuilder builder) throws Exception {
068      this.policySupported = builder.httpClient == null;
069      this.httpClient = builder.httpClient != null ? builder.httpClient : new CredentialGuardingHttpClient();
070      this.responseTimeoutMs = builder.responseTimeoutMs;
071      if (this.policySupported)
072         // Only a client this transport created (never yet started) reaches here.  Installs a SocketAddressResolver
073         // that pin-on-connects the connections of SSRF-guard-active @Remote requests, which are tagged into a
074         // connection pool of their own (see PolicyPinningSocketAddressResolver); ordinary (non-@Remote)
075         // connections on this same client are completely unaffected. It must be installed
076         // before the client starts, because Jetty rejects a resolver change once started. The default async
077         // resolver is built from the client's executor and scheduler, which are not safely readable until after
078         // start, so the fallback here is Jetty's synchronous SocketAddressResolver.Sync, a supported,
079         // dependency-free implementation, rather than a hand-built copy.
080         this.httpClient.setSocketAddressResolver(new PolicyPinningSocketAddressResolver(new SocketAddressResolver.Sync()));
081      if (!this.httpClient.isStarted())
082         this.httpClient.start();
083   }
084
085   /**
086    * Returns a new builder for this transport.
087    *
088    * @return A new builder. Never <jk>null</jk>.
089    */
090   public static JettyHttpTransportBuilder builder() {
091      return new JettyHttpTransportBuilder();
092   }
093
094   /**
095    * Returns a new instance backed by a default {@link HttpClient}.
096    *
097    * @return A new instance. Never <jk>null</jk>.
098    * @throws Exception If the Jetty {@link HttpClient} cannot be started.
099    */
100   public static JettyHttpTransport create() throws Exception {
101      return builder().build();
102   }
103
104   @Override /* HttpTransport */
105   public TransportResponse execute(TransportRequest request) throws TransportException {
106      if (request.isSsrfGuardActive())
107         return executeWithPolicy(request); // HTT: returns normally only after a pinned connect to a genuinely public address (guarded requests have their own connection pool, so they never reuse an unpinned connection), which a hermetic loopback test cannot make
108      try {
109         return sendOnce(request);
110      } catch (StaleConnectionException e) {
111         // A pooled keep-alive connection was torn down by the server before any response was received.
112         // Replay once on a fresh connection, but only when it is provably safe (idempotent + repeatable body).
113         if (! request.isSafeToReplay())
114            throw e.asTransportException();
115         try {
116            return sendOnce(request);
117         } catch (StaleConnectionException e2) {
118            throw e2.asTransportException();
119         }
120      }
121   }
122
123   @Override /* HttpTransport */
124   public boolean supportsUrlPolicy() {
125      return policySupported;
126   }
127
128   // Juneau-controlled redirect revalidation for a guarded request; every hop is pinned (see buildJettyRequest).
129   private TransportResponse executeWithPolicy(TransportRequest request) throws TransportException {
130      return PolicyEnforcedRedirects.execute(request, this::sendOncePinned); // HTT: same as above; returns normally only after a pinned connect to a public address
131   }
132
133   // Performs one pin-on-connect hop: the hop request is still SSRF-guard-active, so it is tagged into the pinned
134   // connection pool.  Jetty's own redirect-following is disabled so PolicyEnforcedRedirects owns the loop.
135   private TransportResponse sendOncePinned(TransportRequest request) throws TransportException {
136      try {
137         return sendOnce(request, false); // HTT: same as above; completes only after a pinned connect to a public address
138      } catch (StaleConnectionException e) { // HTT: needs a pooled connection in the pinned pool that the server then drops, and every connection in that pool was pinned to a public address, which a hermetic loopback test cannot reach
139         throw e.asTransportException(); // HTT: see the catch above
140      }
141   }
142
143   // Performs a single HTTP exchange.  Throws StaleConnectionException (a retryable signal) when the failure is a
144   // pre-response stale-connection failure; throws TransportException for every other failure.
145   private TransportResponse sendOnce(TransportRequest request) throws TransportException, StaleConnectionException {
146      return sendOnce(request, true);
147   }
148
149   private TransportResponse sendOnce(TransportRequest request, boolean followRedirects) throws TransportException, StaleConnectionException {
150      var jettyRequest = buildJettyRequest(request).followRedirects(followRedirects);
151      var listener = new InputStreamResponseListener();
152      jettyRequest.send(listener);
153      Response jettyResponse;
154      try {
155         // A per-request timeout overrides the transport-level response header timeout.
156         var perRequest = request.getTimeout();
157         long waitMs;
158         if (perRequest != null)
159            waitMs = Math.max(1, perRequest.toMillis());
160         else if (responseTimeoutMs > 0)
161            waitMs = responseTimeoutMs;
162         else
163            waitMs = Long.MAX_VALUE;
164         jettyResponse = listener.get(waitMs, TimeUnit.MILLISECONDS);
165      } catch (InterruptedException e) {
166         Thread.currentThread().interrupt();
167         abortQuietly(listener);
168         throw new TransportException("HTTP request interrupted", e);
169      } catch (TimeoutException e) {
170         abortQuietly(listener);
171         throw new TransportException("HTTP request timed out", e);
172      } catch (ExecutionException e) {
173         abortQuietly(listener);
174         var cause = e.getCause();
175         if (isStaleConnectionFailure(cause))
176            throw new StaleConnectionException(cause);
177         throw new TransportException("HTTP transport error: " + cause.getMessage(), cause);
178      }
179      return buildTransportResponse(jettyResponse, listener.getInputStream());
180   }
181
182   // A failure is a stale-connection (pre-response) failure when the server closed the pooled connection before
183   // sending any response.  Jetty surfaces this as an end-of-file exception (Jetty's EofException is a subclass)
184   // or a closed-channel exception.  Because the response listener only returns once response headers have
185   // arrived, any such failure here is guaranteed to have occurred before any response bytes were read.
186   private static boolean isStaleConnectionFailure(Throwable cause) {
187      return cause instanceof EOFException || cause instanceof ClosedChannelException;
188   }
189
190   // Releases the Jetty response content when the response never reaches the caller (interrupt/timeout/error),
191   // so the connection is not abandoned.  Closing the listener's input stream aborts and releases the exchange.
192   private static void abortQuietly(InputStreamResponseListener listener) {
193      try {
194         listener.getInputStream().close();
195      } catch (IOException e) { // HTT: InputStreamResponseListener's input stream close() never throws IOException in practice; the catch only satisfies the declared signature
196         // Best-effort cleanup on an already-failing path; nothing more can be done.
197      }
198   }
199
200   @Override /* Closeable */
201   public void close() throws IOException {
202      try {
203         httpClient.stop();
204      } catch (Exception e) {
205         throw ioex(e, "Failed to stop Jetty HttpClient: %s", e.getMessage());
206      }
207   }
208
209   // -----------------------------------------------------------------------------------------------------------------
210   // Internal helpers
211   // -----------------------------------------------------------------------------------------------------------------
212
213   private Request buildJettyRequest(TransportRequest request) throws TransportException {
214      Request jettyRequest;
215      try {
216         // A client this transport created strips credential headers from every redirect copy itself; a caller-supplied
217         // client is left unchanged, so its requests are created as CredentialGuardingHttpRequest, which strips them from
218         // its own copies.  That is what HttpClient.newRequest(URI) builds too, but in a guarded subclass: Jetty copies a
219         // request for a redirect with the request's own copy(URI), and does not carry its listeners or attributes over,
220         // so the guard cannot be attached to whatever a client's newRequest returns.  An override of
221         // newRequest(URI) on a caller-supplied client is therefore not called; see JettyHttpTransportBuilder.httpClient.
222         jettyRequest = (policySupported ? httpClient.newRequest(request.getUri()) : new CredentialGuardingHttpRequest(httpClient, request.getUri()))
223            .method(request.getMethod());
224      } catch (IllegalArgumentException e) {
225         // Jetty refuses a URI that java.net.URI parsed with a null host (for example an underscore host name).
226         throw new TransportException("HTTP transport error: " + e.getMessage(), e);
227      }
228      // The tag is part of Jetty's destination key, so a guarded request gets a connection pool of its own whose
229      // connections are all opened through the pin; it can never reuse a connection an unguarded request opened.
230      if (request.isSsrfGuardActive())
231         jettyRequest.tag(PolicyPinningSocketAddressResolver.tagFor(request.getAllowedAddressRanges()));
232      for (var h : request.getHeaders())
233         jettyRequest.headers(fields -> fields.add(h.name(), h.value()));
234      var body = request.getBody();
235      if (body != null)
236         jettyRequest.body(buildRequestContent(body));
237      return jettyRequest;
238   }
239
240   private static OutputStreamRequestContent buildRequestContent(TransportBody body) {
241      var ct = body.getContentType();
242      var contentType = ct != null ? ct : "application/octet-stream";
243      var content = new OutputStreamRequestContent(contentType);
244      // Write body to the content's OutputStream on a background thread so Jetty can stream it concurrently.
245      var writer = new Thread(() -> {
246         try (var out = content.getOutputStream()) {
247            body.writeTo(out);
248         } catch (IOException e) {
249            // Closing the stream on error signals Jetty to abort the request.
250         }
251      }, "juneau-ng-body-writer");
252      writer.setDaemon(true);
253      writer.start();
254      return content;
255   }
256
257   private static TransportResponse buildTransportResponse(Response jettyResponse, InputStream bodyStream) {
258      var builder = TransportResponse.builder()
259         .statusCode(jettyResponse.getStatus())
260         .reasonPhrase(jettyResponse.getReason())
261         .body(bodyStream)
262         // Release the Jetty response InputStream when the RestResponse is closed; TransportResponse.close() only runs this callback.
263         .closeCallback(bodyStream);
264      jettyResponse.getHeaders().forEach(field -> builder.header(field.getName(), field.getValue()));
265      return builder.build();
266   }
267
268   // Internal signal that a pre-response stale-connection failure occurred and may be retried once.  Carries the
269   // original cause so the caller can build the user-visible TransportException if the retry is not attempted or
270   // also fails.
271   private static final class StaleConnectionException extends Exception {
272      private static final long serialVersionUID = 1L;
273
274      StaleConnectionException(Throwable cause) {
275         super(cause);
276      }
277
278      TransportException asTransportException() {
279         return new TransportException("HTTP transport error: " + getCause().getMessage(), getCause());
280      }
281   }
282}