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}