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;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.net.*;
022import java.time.*;
023import java.util.*;
024
025import org.apache.juneau.http.remote.*;
026
027/**
028 * A fully-resolved, transport-layer HTTP request.
029 *
030 * <p>
031 * All values (URIs, headers, bodies) are evaluated before this object is constructed — suppliers have been
032 * invoked, {@code null}-valued parts have been filtered, and the final URI has been assembled with query
033 * parameters and path substitutions applied.
034 *
035 * <p>
036 * Transport implementations receive a {@link TransportRequest} and are responsible for executing it against
037 * the remote server, returning a {@link TransportResponse}.
038 *
039 * @since 9.2.1
040 */
041public final class TransportRequest {
042
043   // Methods defined as idempotent by RFC 7231 §4.2.2 — replaying them cannot change server state beyond a
044   // single application of the request, so a transport may safely re-send them once on a fresh connection.
045   private static final Set<String> IDEMPOTENT_METHODS = Set.of("GET", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE");
046
047   private final String method;
048   private final URI uri;
049   private final List<TransportHeader> headers;
050   private final TransportBody body;
051   private final Duration timeout;
052   private final boolean policyEnforced;
053   private final boolean allowPrivateUrls;
054   private final RemoteUrlPolicy.AllowedRanges allowedAddressRanges;
055
056   private TransportRequest(Builder builder) {
057      this.method = reqnn("method", builder.method);
058      this.uri = reqnn("uri", builder.uri);
059      this.headers = List.copyOf(builder.headers);
060      this.body = builder.body;
061      this.timeout = builder.timeout;
062      this.policyEnforced = builder.policyEnforced;
063      this.allowPrivateUrls = builder.allowPrivateUrls;
064      this.allowedAddressRanges = builder.allowedAddressRanges;
065   }
066
067   /** Returns a new {@link Builder}. */
068   public static Builder builder() {
069      return new Builder();
070   }
071
072   /**
073    * Returns the HTTP method (e.g. {@code "GET"}, {@code "POST"}).
074    *
075    * @return The method. Never <jk>null</jk>.
076    */
077   public String getMethod() {
078      return method;
079   }
080
081   /**
082    * Returns the fully-resolved request URI, including query string.
083    *
084    * @return The URI. Never <jk>null</jk>.
085    */
086   public URI getUri() {
087      return uri;
088   }
089
090   /**
091    * Returns all request headers in the order they should be sent.
092    *
093    * @return An unmodifiable list. Never <jk>null</jk>.
094    */
095   public List<TransportHeader> getHeaders() {
096      return headers;
097   }
098
099   /**
100    * Returns the first header with the given name (case-insensitive), or {@code null} if absent.
101    *
102    * @param name The header name. Must not be <jk>null</jk>.
103    * @return The first matching header, or <jk>null</jk> if absent.
104    */
105   public TransportHeader getFirstHeader(String name) {
106      return headers.stream()
107         .filter(h -> eqic(h.name(), name))
108         .findFirst()
109         .orElse(null);
110   }
111
112   /**
113    * Returns the request body, or {@code null} if this is a body-less request (e.g. GET, HEAD).
114    *
115    * @return The body, possibly <jk>null</jk>.
116    */
117   public TransportBody getBody() {
118      return body;
119   }
120
121   /**
122    * Returns {@code true} if this request can be transparently re-sent once after a stale-connection failure.
123    *
124    * <p>
125    * A pooled keep-alive socket can be torn down by the server between requests; when a transport then reuses
126    * that socket the first write fails before any response is received (e.g. an early {@code EOF} or a
127    * {@code NoHttpResponseException}).  Such a failure is safe to retry on a fresh connection <b>only</b> when
128    * replaying the request cannot cause a duplicate side effect, which requires both of:
129    * <ul>
130    *    <li>an <a class="doclink" href="https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.2">idempotent</a>
131    *       HTTP method ({@code GET}, {@code HEAD}, {@code PUT}, {@code DELETE}, {@code OPTIONS}, {@code TRACE}) — never
132    *       {@code POST} or any other non-idempotent method, and
133    *    <li>a body that is either absent or {@link TransportBody#isRepeatable() repeatable}, so the exact same bytes
134    *       can be written again.
135    * </ul>
136    *
137    * <p>
138    * When in doubt this returns {@code false} — the caller must fail closed rather than risk double-executing a
139    * request.
140    *
141    * @return {@code true} if the request is provably safe to replay once.
142    */
143   public boolean isSafeToReplay() {
144      return IDEMPOTENT_METHODS.contains(method.toUpperCase(Locale.ROOT)) && (body == null || body.isRepeatable());
145   }
146
147   /**
148    * Returns the per-call response timeout, or {@code null} if none was set.
149    *
150    * <p>
151    * A {@code null} value means the transport's own default applies.  Connect timeouts remain a client-level
152    * concern.  All built-in transports honor this value (JDK, Apache HttpClient 4.5/5, OkHttp, Jetty and the mock transport).
153    *
154    * @return The response timeout, possibly <jk>null</jk>.
155    */
156   public Duration getTimeout() {
157      return timeout;
158   }
159
160   /**
161    * Returns {@code true} if this request originated from a {@code @Remote}/{@code @Url} call and is subject to
162    * the SSRF guardrail (see {@code org.apache.juneau.http.remote.RemoteUrlPolicy}).
163    *
164    * <p>
165    * Ordinary (non-{@code @Remote}) requests built directly via {@code RestClient.get(url)} etc. are never
166    * policy-covered.
167    *
168    * @return {@code true} if the SSRF guardrail applies to this request.
169    */
170   public boolean isPolicyEnforced() {
171      return policyEnforced;
172   }
173
174   /**
175    * Returns {@code true} if the {@code allowPrivateUrls} opt-in is in effect for this request, disabling the
176    * deny-private / pin-on-connect / redirect-revalidation checks (the {@code http}/{@code https} scheme
177    * requirement still applies upstream).
178    *
179    * @return {@code true} if private/loopback/link-local/metadata targets are allowed for this request.
180    */
181   public boolean isAllowPrivateUrls() {
182      return allowPrivateUrls;
183   }
184
185   /**
186    * Returns the per-client special-purpose address ranges that the SSRF guardrail re-permits for this request (see
187    * {@link RestClient.Builder#allowAddressRanges(String...)}); they are unioned with the
188    * {@value RemoteUrlPolicy#ALLOW_ADDRESS_RANGES_PROPERTY} system property.
189    *
190    * @return The allowed ranges. Never <jk>null</jk>; {@link org.apache.juneau.http.remote.RemoteUrlPolicy.AllowedRanges#NONE RemoteUrlPolicy.AllowedRanges#NONE} by default.
191    */
192   public RemoteUrlPolicy.AllowedRanges getAllowedAddressRanges() {
193      return allowedAddressRanges;
194   }
195
196   /**
197    * Returns {@code true} if the SSRF guardrail's connect-time machinery (pin-on-connect + Juneau-controlled
198    * redirect revalidation) must be applied by the transport for this request: {@link #isPolicyEnforced()} is set
199    * and {@link #isAllowPrivateUrls()} is not.
200    *
201    * @return {@code true} if the transport must pin-on-connect and manually revalidate redirects for this request.
202    */
203   public boolean isSsrfGuardActive() {
204      return policyEnforced && ! allowPrivateUrls;
205   }
206
207   // -----------------------------------------------------------------------------------------------------------------
208   // Builder
209   // -----------------------------------------------------------------------------------------------------------------
210
211   /**
212    * Fluent builder for {@link TransportRequest}.
213    *
214    * @since 9.2.1
215    */
216   public static final class Builder {
217
218      String method;
219      URI uri;
220      final List<TransportHeader> headers = l();
221      TransportBody body;
222      Duration timeout;
223      boolean policyEnforced;
224      boolean allowPrivateUrls;
225      RemoteUrlPolicy.AllowedRanges allowedAddressRanges = RemoteUrlPolicy.AllowedRanges.NONE;
226
227      private Builder() {}
228
229      /**
230       * Sets the HTTP method.
231       *
232       * @param value The method (e.g. {@code "GET"}). Must not be <jk>null</jk>.
233       * @return This object.
234       */
235      public Builder method(String value) {
236         method = value;
237         return this;
238      }
239
240      /**
241       * Sets the request URI.
242       *
243       * @param value The URI. Must not be <jk>null</jk>.
244       * @return This object.
245       */
246      public Builder uri(URI value) {
247         uri = value;
248         return this;
249      }
250
251      /**
252       * Sets the request URI from a string.
253       *
254       * @param value The URI string. Must not be <jk>null</jk>.
255       * @return This object.
256       * @throws IllegalArgumentException If the string is not a valid URI.
257       */
258      public Builder uri(String value) {
259         try {
260            uri = new URI(value);
261         } catch (URISyntaxException e) {
262            throw iaex(e, "Invalid URI: %s", value);
263         }
264         return this;
265      }
266
267      /**
268       * Appends a request header.
269       *
270       * @param name The header name. Must not be <jk>null</jk>.
271       * @param value The header value. Must not be <jk>null</jk>.
272       * @return This object.
273       */
274      public Builder header(String name, String value) {
275         headers.add(TransportHeader.of(name, value));
276         return this;
277      }
278
279      /**
280       * Appends multiple request headers.
281       *
282       * @param value The headers to add. Must not be <jk>null</jk>.
283       * @return This object.
284       */
285      public Builder headers(Collection<TransportHeader> value) {
286         headers.addAll(value);
287         return this;
288      }
289
290      /**
291       * Sets the request body.
292       *
293       * @param value The body. May be <jk>null</jk> for body-less requests.
294       * @return This object.
295       */
296      public Builder body(TransportBody value) {
297         body = value;
298         return this;
299      }
300
301      /**
302       * Sets the per-call response timeout.
303       *
304       * @param value The response timeout. May be <jk>null</jk> to use the transport default.  Must otherwise be positive.
305       * @return This object.
306       * @throws IllegalArgumentException If <c>value</c> is zero or negative.
307       */
308      public Builder timeout(Duration value) {
309         if (value != null && (value.isZero() || value.isNegative()))
310            throw new IllegalArgumentException("Request timeout must be positive: " + value);
311         timeout = value;
312         return this;
313      }
314
315      /**
316       * Marks this request as originating from a {@code @Remote}/{@code @Url} call, subject to the SSRF guardrail,
317       * and records whether the {@code allowPrivateUrls} opt-in is in effect.
318       *
319       * @param policyEnforced {@code true} if this request is policy-covered.
320       * @param allowPrivateUrls {@code true} if the {@code allowPrivateUrls} opt-in disables the deny-private checks.
321       * @return This object.
322       */
323      public Builder remoteUrlPolicy(boolean policyEnforced, boolean allowPrivateUrls) {
324         this.policyEnforced = policyEnforced;
325         this.allowPrivateUrls = allowPrivateUrls;
326         return this;
327      }
328
329      /**
330       * Sets the per-client special-purpose address ranges the SSRF guardrail re-permits for this request.
331       *
332       * @param value The allowed ranges. <jk>null</jk> means none.
333       * @return This object.
334       */
335      public Builder allowedAddressRanges(RemoteUrlPolicy.AllowedRanges value) {
336         this.allowedAddressRanges = value == null ? RemoteUrlPolicy.AllowedRanges.NONE : value;
337         return this;
338      }
339
340      /**
341       * Builds and returns the {@link TransportRequest}.
342       *
343       * @return A new instance. Never <jk>null</jk>.
344       */
345      public TransportRequest build() {
346         return new TransportRequest(this);
347      }
348   }
349}