001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.eclipse.aether.util.connector.transport.http; 020 021import java.net.InetAddress; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.net.UnknownHostException; 025import java.nio.charset.Charset; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.Map; 029import java.util.Optional; 030import java.util.Set; 031 032import org.eclipse.aether.ConfigurationProperties; 033import org.eclipse.aether.RepositorySystemSession; 034import org.eclipse.aether.repository.RemoteRepository; 035import org.eclipse.aether.util.ConfigUtils; 036 037/** 038 * A utility class to read HTTP transport related configuration. It implements all HTTP transport related configurations from 039 * {@link ConfigurationProperties} and transport implementations are free to use those that are supported by themselves. 040 * 041 * @see ConfigurationProperties 042 * @see RepositorySystemSession#getConfigProperties() 043 * @since 2.0.15 044 */ 045public final class HttpTransporterUtils { 046 private HttpTransporterUtils() {} 047 048 /** 049 * Getter for {@link ConfigurationProperties#USER_AGENT}. 050 */ 051 public static String getUserAgent(RepositorySystemSession session, RemoteRepository repository) { 052 return ConfigUtils.getString( 053 session, 054 ConfigurationProperties.DEFAULT_USER_AGENT, 055 ConfigurationProperties.USER_AGENT, 056 "aether.connector.userAgent"); 057 } 058 059 /** 060 * Getter for {@link ConfigurationProperties#HTTPS_SECURITY_MODE}. 061 */ 062 public static String getHttpsSecurityMode(RepositorySystemSession session, RemoteRepository repository) { 063 String result = ConfigUtils.getString( 064 session, 065 ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT, 066 ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(), 067 ConfigurationProperties.HTTPS_SECURITY_MODE); 068 if (!ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT.equals(result) 069 && !ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(result)) { 070 throw new IllegalArgumentException("Unsupported '" + result + "' HTTPS security mode."); 071 } 072 return result; 073 } 074 075 /** 076 * Getter for {@link ConfigurationProperties#HTTP_CONNECTION_MAX_TTL}. 077 */ 078 public static int getHttpConnectionMaxTtlSeconds(RepositorySystemSession session, RemoteRepository repository) { 079 int result = ConfigUtils.getInteger( 080 session, 081 ConfigurationProperties.DEFAULT_HTTP_CONNECTION_MAX_TTL, 082 ConfigurationProperties.HTTP_CONNECTION_MAX_TTL + "." + repository.getId(), 083 ConfigurationProperties.HTTP_CONNECTION_MAX_TTL); 084 if (result < 0) { 085 throw new IllegalArgumentException(ConfigurationProperties.HTTP_CONNECTION_MAX_TTL + " value must be >= 0"); 086 } 087 return result; 088 } 089 090 /** 091 * Getter for {@link ConfigurationProperties#HTTP_MAX_CONNECTIONS_PER_ROUTE}. 092 */ 093 public static int getHttpMaxConnectionsPerRoute(RepositorySystemSession session, RemoteRepository repository) { 094 int result = ConfigUtils.getInteger( 095 session, 096 ConfigurationProperties.DEFAULT_HTTP_MAX_CONNECTIONS_PER_ROUTE, 097 ConfigurationProperties.HTTP_MAX_CONNECTIONS_PER_ROUTE + "." + repository.getId(), 098 ConfigurationProperties.HTTP_MAX_CONNECTIONS_PER_ROUTE); 099 if (result < 1) { 100 throw new IllegalArgumentException( 101 ConfigurationProperties.HTTP_MAX_CONNECTIONS_PER_ROUTE + " value must be > 0"); 102 } 103 return result; 104 } 105 106 /** 107 * Getter for {@link ConfigurationProperties#HTTP_HEADERS}. 108 */ 109 @SuppressWarnings("unchecked") 110 public static Map<String, String> getHttpHeaders(RepositorySystemSession session, RemoteRepository repository) { 111 return (Map<String, String>) ConfigUtils.getMap( 112 session, 113 Collections.emptyMap(), 114 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(), 115 ConfigurationProperties.HTTP_HEADERS); 116 } 117 118 /** 119 * Getter for {@link ConfigurationProperties#HTTP_REDIRECT_MODE}. 120 * 121 * @since 2.0.25 122 */ 123 public static ConfigurationProperties.RedirectMode getRedirectMode( 124 RepositorySystemSession session, RemoteRepository repository) { 125 return getRedirectMode(session, repository, null); 126 } 127 128 /** 129 * Getter for {@link ConfigurationProperties#HTTP_REDIRECT_MODE}, with an optional deprecated 130 * transport-specific property as a fallback. 131 * 132 * @since 2.0.25 133 */ 134 public static ConfigurationProperties.RedirectMode getRedirectMode( 135 RepositorySystemSession session, RemoteRepository repository, String deprecatedTransportProperty) { 136 String[] configurationProperties = deprecatedTransportProperty == null 137 ? new String[] { 138 ConfigurationProperties.HTTP_REDIRECT_MODE + "." + repository.getId(), 139 ConfigurationProperties.HTTP_REDIRECT_MODE 140 } 141 : new String[] { 142 ConfigurationProperties.HTTP_REDIRECT_MODE + "." + repository.getId(), 143 ConfigurationProperties.HTTP_REDIRECT_MODE, 144 deprecatedTransportProperty + "." + repository.getId(), 145 deprecatedTransportProperty 146 }; 147 return ConfigUtils.getEnum( 148 session, 149 ConfigurationProperties.RedirectMode.class, 150 ConfigurationProperties.DEFAULT_REDIRECT_MODE, 151 configurationProperties); 152 } 153 154 /** 155 * Getter for {@link ConfigurationProperties#HTTP_REDIRECT_MODE}, with an optional deprecated 156 * transport-specific boolean property as a fallback. A {@code true} legacy value maps to {@code ANY}, and a 157 * {@code false} value maps to {@code NONE}. 158 * 159 * @since 2.0.25 160 */ 161 public static ConfigurationProperties.RedirectMode getRedirectMode( 162 RepositorySystemSession session, 163 RemoteRepository repository, 164 boolean defaultFollowRedirects, 165 String deprecatedTransportProperty) { 166 ConfigurationProperties.RedirectMode mode = ConfigUtils.getEnum( 167 session, 168 ConfigurationProperties.RedirectMode.class, 169 null, 170 ConfigurationProperties.HTTP_REDIRECT_MODE + "." + repository.getId(), 171 ConfigurationProperties.HTTP_REDIRECT_MODE); 172 if (mode != null) { 173 return mode; 174 } 175 boolean followRedirects = ConfigUtils.getBoolean( 176 session, 177 defaultFollowRedirects, 178 deprecatedTransportProperty + "." + repository.getId(), 179 deprecatedTransportProperty); 180 return followRedirects ? ConfigurationProperties.RedirectMode.ANY : ConfigurationProperties.RedirectMode.NONE; 181 } 182 183 /** 184 * Getter for {@link ConfigurationProperties#HTTP_ORIGIN_SCOPED_HEADERS}. 185 * 186 * @since 2.0.25 187 */ 188 public static boolean isOriginScopedHeaders(RepositorySystemSession session, RemoteRepository repository) { 189 return isOriginScopedHeaders(session, repository, null); 190 } 191 192 /** 193 * Getter for {@link ConfigurationProperties#HTTP_ORIGIN_SCOPED_HEADERS}, with an optional deprecated 194 * transport-specific property as a fallback. 195 * 196 * @since 2.0.25 197 */ 198 public static boolean isOriginScopedHeaders( 199 RepositorySystemSession session, RemoteRepository repository, String deprecatedTransportProperty) { 200 return getHttpBoolean( 201 session, 202 ConfigurationProperties.DEFAULT_HTTP_ORIGIN_SCOPED_HEADERS, 203 ConfigurationProperties.HTTP_ORIGIN_SCOPED_HEADERS, 204 repository, 205 deprecatedTransportProperty); 206 } 207 208 private static boolean getHttpBoolean( 209 RepositorySystemSession session, 210 boolean defaultValue, 211 String configurationProperty, 212 RemoteRepository repository, 213 String deprecatedTransportProperty) { 214 if (deprecatedTransportProperty == null) { 215 return ConfigUtils.getBoolean( 216 session, defaultValue, configurationProperty + "." + repository.getId(), configurationProperty); 217 } 218 return ConfigUtils.getBoolean( 219 session, 220 defaultValue, 221 configurationProperty + "." + repository.getId(), 222 configurationProperty, 223 deprecatedTransportProperty + "." + repository.getId(), 224 deprecatedTransportProperty); 225 } 226 227 /** 228 * Getter for {@link ConfigurationProperties#HTTP_FOLLOW_INSECURE_REDIRECT}. 229 * 230 * @since 2.0.25 231 */ 232 public static boolean isFollowInsecureRedirect(RepositorySystemSession session, RemoteRepository repository) { 233 return isFollowInsecureRedirect(session, repository, null); 234 } 235 236 /** 237 * Getter for {@link ConfigurationProperties#HTTP_FOLLOW_INSECURE_REDIRECT}, with an optional deprecated 238 * transport-specific property as a fallback. 239 * 240 * @since 2.0.25 241 */ 242 public static boolean isFollowInsecureRedirect( 243 RepositorySystemSession session, RemoteRepository repository, String deprecatedTransportProperty) { 244 return getHttpBoolean( 245 session, 246 ConfigurationProperties.DEFAULT_HTTP_FOLLOW_INSECURE_REDIRECT, 247 ConfigurationProperties.HTTP_FOLLOW_INSECURE_REDIRECT, 248 repository, 249 deprecatedTransportProperty); 250 } 251 252 /** 253 * Getter for {@link ConfigurationProperties#HTTP_PREEMPTIVE_AUTH}. 254 */ 255 public static boolean isHttpPreemptiveAuth(RepositorySystemSession session, RemoteRepository repository) { 256 return ConfigUtils.getBoolean( 257 session, 258 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_AUTH, 259 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH + "." + repository.getId(), 260 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH); 261 } 262 263 /** 264 * Getter for {@link ConfigurationProperties#HTTP_PREEMPTIVE_PUT_AUTH}. 265 */ 266 public static boolean isHttpPreemptivePutAuth(RepositorySystemSession session, RemoteRepository repository) { 267 return ConfigUtils.getBoolean( 268 session, 269 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_PUT_AUTH, 270 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH + "." + repository.getId(), 271 ConfigurationProperties.HTTP_PREEMPTIVE_PUT_AUTH); 272 } 273 274 /** 275 * Getter for {@link ConfigurationProperties#HTTP_SUPPORT_WEBDAV}. 276 */ 277 public static boolean isHttpSupportWebDav(RepositorySystemSession session, RemoteRepository repository) { 278 return ConfigUtils.getBoolean( 279 session, 280 ConfigurationProperties.DEFAULT_HTTP_SUPPORT_WEBDAV, 281 ConfigurationProperties.HTTP_SUPPORT_WEBDAV + "." + repository.getId(), 282 ConfigurationProperties.HTTP_SUPPORT_WEBDAV); 283 } 284 285 /** 286 * Getter for {@link ConfigurationProperties#HTTP_SEND_RFC9457_ACCEPT}. 287 * 288 * @since 2.0.19 289 */ 290 public static boolean isHttpSendRfc9457Accept(RepositorySystemSession session, RemoteRepository repository) { 291 return ConfigUtils.getBoolean( 292 session, 293 ConfigurationProperties.DEFAULT_HTTP_SEND_RFC9457_ACCEPT, 294 ConfigurationProperties.HTTP_SEND_RFC9457_ACCEPT + "." + repository.getId(), 295 ConfigurationProperties.HTTP_SEND_RFC9457_ACCEPT); 296 } 297 298 /** 299 * Getter for {@link ConfigurationProperties#HTTP_CREDENTIAL_ENCODING}. 300 */ 301 public static Charset getHttpCredentialsEncoding(RepositorySystemSession session, RemoteRepository repository) { 302 return Charset.forName(ConfigUtils.getString( 303 session, 304 ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING, 305 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "." + repository.getId(), 306 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING)); 307 } 308 309 /** 310 * Getter for {@link ConfigurationProperties#CONNECT_TIMEOUT}. 311 */ 312 public static int getHttpConnectTimeout(RepositorySystemSession session, RemoteRepository repository) { 313 return ConfigUtils.getInteger( 314 session, 315 ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT, 316 ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(), 317 ConfigurationProperties.CONNECT_TIMEOUT); 318 } 319 320 /** 321 * Getter for {@link ConfigurationProperties#REQUEST_TIMEOUT}. 322 */ 323 public static int getHttpRequestTimeout(RepositorySystemSession session, RemoteRepository repository) { 324 return ConfigUtils.getInteger( 325 session, 326 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT, 327 ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(), 328 ConfigurationProperties.REQUEST_TIMEOUT); 329 } 330 331 /** 332 * Getter for {@link ConfigurationProperties#HTTP_RETRY_HANDLER_COUNT}. 333 */ 334 public static int getHttpRetryHandlerCount(RepositorySystemSession session, RemoteRepository repository) { 335 int result = ConfigUtils.getInteger( 336 session, 337 ConfigurationProperties.DEFAULT_HTTP_RETRY_HANDLER_COUNT, 338 ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT + "." + repository.getId(), 339 ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT); 340 if (result < 0) { 341 throw new IllegalArgumentException( 342 ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT + " value must be >= 0"); 343 } 344 return result; 345 } 346 347 /** 348 * Getter for {@link ConfigurationProperties#HTTP_RETRY_HANDLER_INTERVAL}. 349 */ 350 public static long getHttpRetryHandlerInterval(RepositorySystemSession session, RemoteRepository repository) { 351 long result = ConfigUtils.getLong( 352 session, 353 ConfigurationProperties.DEFAULT_HTTP_RETRY_HANDLER_INTERVAL, 354 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL + "." + repository.getId(), 355 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL); 356 if (result < 0) { 357 throw new IllegalArgumentException( 358 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL + " value must be >= 0"); 359 } 360 return result; 361 } 362 363 /** 364 * Getter for {@link ConfigurationProperties#HTTP_RETRY_HANDLER_INTERVAL_MAX}. 365 */ 366 public static long getHttpRetryHandlerIntervalMax(RepositorySystemSession session, RemoteRepository repository) { 367 long result = ConfigUtils.getLong( 368 session, 369 ConfigurationProperties.DEFAULT_HTTP_RETRY_HANDLER_INTERVAL_MAX, 370 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL_MAX + "." + repository.getId(), 371 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL_MAX); 372 if (result < 0) { 373 throw new IllegalArgumentException( 374 ConfigurationProperties.HTTP_RETRY_HANDLER_INTERVAL_MAX + " value must be >= 0"); 375 } 376 return result; 377 } 378 379 /** 380 * Getter for {@link ConfigurationProperties#HTTP_EXPECT_CONTINUE}. 381 */ 382 public static Optional<Boolean> getHttpExpectContinue( 383 RepositorySystemSession session, RemoteRepository repository) { 384 String expectContinue = ConfigUtils.getString( 385 session, 386 null, 387 ConfigurationProperties.HTTP_EXPECT_CONTINUE + "." + repository.getId(), 388 ConfigurationProperties.HTTP_EXPECT_CONTINUE); 389 if (expectContinue != null) { 390 return Optional.of(Boolean.parseBoolean(expectContinue)); 391 } 392 return Optional.empty(); 393 } 394 395 /** 396 * Getter for {@link ConfigurationProperties#HTTP_REUSE_CONNECTIONS}. 397 */ 398 public static boolean isHttpReuseConnections(RepositorySystemSession session, RemoteRepository repository) { 399 return ConfigUtils.getBoolean( 400 session, 401 ConfigurationProperties.DEFAULT_HTTP_REUSE_CONNECTIONS, 402 ConfigurationProperties.HTTP_REUSE_CONNECTIONS + "." + repository.getId(), 403 ConfigurationProperties.HTTP_REUSE_CONNECTIONS); 404 } 405 406 /** 407 * Getter for {@link ConfigurationProperties#HTTP_RETRY_HANDLER_SERVICE_UNAVAILABLE}. 408 */ 409 public static Set<Integer> getHttpServiceUnavailableCodes( 410 RepositorySystemSession session, RemoteRepository repository) { 411 String stringValue = ConfigUtils.getString( 412 session, 413 ConfigurationProperties.DEFAULT_HTTP_RETRY_HANDLER_SERVICE_UNAVAILABLE, 414 ConfigurationProperties.HTTP_RETRY_HANDLER_SERVICE_UNAVAILABLE + "." + repository.getId(), 415 ConfigurationProperties.HTTP_RETRY_HANDLER_SERVICE_UNAVAILABLE); 416 Set<Integer> result = new HashSet<>(); 417 try { 418 for (String code : ConfigUtils.parseCommaSeparatedUniqueNames(stringValue)) { 419 result.add(Integer.parseInt(code)); 420 } 421 } catch (NumberFormatException e) { 422 throw new IllegalArgumentException( 423 "Illegal HTTP codes for " + ConfigurationProperties.HTTP_RETRY_HANDLER_SERVICE_UNAVAILABLE 424 + " (list of integers): " + stringValue); 425 } 426 return result; 427 } 428 429 /** 430 * Getter for {@link ConfigurationProperties#HTTP_LOCAL_ADDRESS}. 431 */ 432 public static Optional<InetAddress> getHttpLocalAddress( 433 RepositorySystemSession session, RemoteRepository repository) { 434 String bindAddress = ConfigUtils.getString( 435 session, 436 null, 437 ConfigurationProperties.HTTP_LOCAL_ADDRESS + "." + repository.getId(), 438 ConfigurationProperties.HTTP_LOCAL_ADDRESS); 439 if (bindAddress != null) { 440 try { 441 return Optional.of(InetAddress.getByName(bindAddress)); 442 } catch (UnknownHostException uhe) { 443 throw new IllegalArgumentException( 444 "Given bind address (" + bindAddress + ") cannot be resolved for remote repository " 445 + repository, 446 uhe); 447 } 448 } 449 return Optional.empty(); 450 } 451 452 /** 453 * Getter for {@link ConfigurationProperties#HTTP_VERSION}. 454 */ 455 public static ConfigurationProperties.HttpVersion getHttpVersion( 456 RepositorySystemSession session, RemoteRepository repository) { 457 return ConfigUtils.getEnum( 458 session, 459 ConfigurationProperties.HttpVersion.class, 460 ConfigurationProperties.DEFAULT_HTTP_VERSION, 461 ConfigurationProperties.HTTP_VERSION + "." + repository.getId(), 462 ConfigurationProperties.HTTP_VERSION); 463 } 464 465 /** 466 * Clamps a remote-supplied {@code Last-Modified} timestamp to the local clock: returns the given value as-is, 467 * or the current time when the value lies in the future. 468 * <p> 469 * HTTP transports stamp the downloaded file's modification time from the server-provided {@code Last-Modified} 470 * header, and the update check manager subsequently uses that file modification time as the {@code lastUpdated} 471 * instant for update-policy cutoffs. A repository serving a far-future {@code Last-Modified} date would otherwise 472 * permanently suppress interval-based update checks for that path. Update-interval decisions must rest on 473 * locally-observed time, so future-dated values are clamped to {@code System.currentTimeMillis()}. 474 * 475 * @param lastModifiedMillis the last modified timestamp in milliseconds since the epoch, as supplied by the 476 * remote server 477 * @return the supplied value, clamped to the local wall clock 478 * @since 2.0.23 479 */ 480 public static long clampRemoteLastModified(long lastModifiedMillis) { 481 return Math.min(lastModifiedMillis, System.currentTimeMillis()); 482 } 483 484 /** 485 * Shared code to create "base {@link URI}" for most common HTTP remote repositories and all HTTP transports. 486 * Note: this method just applies common validation and adjustments to URI, but it does not enforce protocol 487 * to be HTTP/HTTPS! 488 * <p> 489 * Validations and adjustments applied: 490 * <ul> 491 * <li>URI string is parsed from {@link RemoteRepository#getUrl()} returned string</li> 492 * <li>URI must have parsable {@link URI#parseServerAuthority()}</li> 493 * <li>URI must not be opaque</li> 494 * <li>URI must not have fragment or query</li> 495 * <li>URI path is adjusted to end with {@code /} (slash).</li> 496 * </ul> 497 * 498 * @since 2.0.18 499 */ 500 public static URI getBaseUri(RemoteRepository repository) throws URISyntaxException { 501 URI uri = new URI(repository.getUrl()).parseServerAuthority(); 502 if (uri.isOpaque()) { 503 throw new URISyntaxException(repository.getUrl(), "URL must not be opaque"); 504 } 505 if (uri.getRawFragment() != null || uri.getRawQuery() != null) { 506 throw new URISyntaxException(repository.getUrl(), "URL must not have fragment or query"); 507 } 508 String path = uri.getRawPath(); 509 if (path == null) { 510 path = "/"; 511 } 512 if (!path.startsWith("/")) { 513 path = "/" + path; 514 } 515 if (!path.endsWith("/")) { 516 path = path + "/"; 517 } 518 return new URI(uri.getScheme() + "://" + uri.getRawAuthority() + path); 519 } 520}