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}