All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.helidon.http.RequestedUriDiscoveryContext Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023, 2024 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.helidon.http;

import java.util.ArrayList;
import java.util.List;

import io.helidon.common.config.Config;
import io.helidon.common.configurable.AllowList;
import io.helidon.common.uri.UriInfo;
import io.helidon.common.uri.UriQuery;
import io.helidon.config.metadata.Configured;
import io.helidon.config.metadata.ConfiguredOption;

/**
 * Requested URI discovery settings for a socket.
 */
public interface RequestedUriDiscoveryContext {

    /**
     * Creates a new builder for a {@code RequestedUriDiscoveryContext}.
     *
     * @return new builder
     */
    static Builder builder() {
        return new Builder();
    }

    /**
     * Creates a new builder for a {@code RequestedUriDiscoveryContext} using the provide discovery context config node.
     *
     * @param config discovery context config node
     * @return new builder
     */
    static Builder builder(Config config) {
        return builder().config(config);
    }

    /**
     * Creates a new {@code RequestedUriDiscoveryContext} from the provided discovery context config node.
     *
     * @param config node for the discovery context
     * @return new discovery context instance
     */
    static RequestedUriDiscoveryContext create(Config config) {
        return builder().config(config).build();
    }

    /**
     * Creates a {@link io.helidon.common.uri.UriInfo} object for a request based on the discovery settings in the
     * {@link RequestedUriDiscoveryContext} and the specified request-related information.
     *
     * @param remoteAddress remote address from the request
     * @param localAddress local address from the request
     * @param requestPath path from the request
     * @param headers request headers
     * @param query query information from the request
     * @param isSecure whether the request is secure
     * @return {@code UriInfo} which reconstructs, as well as possible, the requested URI from the originating client
     */
    UriInfo uriInfo(String remoteAddress,
                    String localAddress,
                    String requestPath,
                    ServerRequestHeaders headers,
                    UriQuery query,
                    boolean isSecure);

    /**
     * Builder for {@link RequestedUriDiscoveryContext}.
     */
    @Configured
    final class Builder implements io.helidon.common.Builder {

        /**
         * Config key prefix for requested URI discovery settings.
         */
        public static final String REQUESTED_URI_DISCOVERY_CONFIG_KEY = "requested-uri-discovery";

        private static final System.Logger LOGGER = System.getLogger(Builder.class.getName());
        private Boolean enabled;
        private final List discoveryTypes = new ArrayList<>();
        private AllowList trustedProxies;
        private String socketId = "@default";

        private Builder() {
        }

        @Override
        public RequestedUriDiscoveryContext build() {
            prepareAndCheckRequestedUriSettings();
            return new RequestedUriDiscoveryContextImpl(this);
        }

        /**
         * Update the settings from the {@value REQUESTED_URI_DISCOVERY_CONFIG_KEY}
         * {@link io.helidon.common.config.Config} node within the socket configuration.
         *
         * @param requestedUriDiscoveryConfig requested URI discovery configuration node
         * @return updated builder instance
         */
        public Builder config(Config requestedUriDiscoveryConfig) {
            requestedUriDiscoveryConfig.get("enabled")
                    .as(Boolean.class)
                    .ifPresent(this::enabled);
            // TODO - discoveryTypes as a key was never documented but was hand-coded this way. Keep for compatibility
            // in case existing apps happen to use it. Remove as soon as practical.
            requestedUriDiscoveryConfig.get("discoveryTypes")
                    .asList(RequestedUriDiscoveryType.class)
                    .ifPresent(this::discoveryTypes);
            requestedUriDiscoveryConfig.get("types")
                    .asList(RequestedUriDiscoveryType.class)
                    .ifPresent(this::types);
            requestedUriDiscoveryConfig.get("trusted-proxies")
                    .map(AllowList::create)
                    .ifPresent(this::trustedProxies);
            return this;
        }

        /**
         * Sets whether requested URI discovery is enabled for requestes arriving on the socket.
         *
         * @param value new enabled state
         * @return updated builder
         */
        @ConfiguredOption(value = "true if 'types' or 'trusted-proxies' is set; false otherwise")
        public Builder enabled(boolean value) {
            enabled = value;
            return this;
        }

        /**
         * Sets the trusted proxies for requested URI discovery for requests arriving on the socket.
         *
         * @param trustedProxies the {@link io.helidon.common.configurable.AllowList} represented trusted proxies
         * @return updated builder
         */
        @ConfiguredOption
        public Builder trustedProxies(AllowList trustedProxies) {
            this.trustedProxies = trustedProxies;
            return this;
        }

        /**
         * Sets the discovery types for requested URI discovery for requests arriving on the socket.
         *
         * @param discoveryTypes discovery types to use
         * @return updated builder
         */
        @ConfiguredOption()
        public Builder types(List discoveryTypes) {
            this.discoveryTypes.clear();
            this.discoveryTypes.addAll(discoveryTypes);
            return this;
        }

        /**
         * Sets the discovery types for requested URI discovery for requests arriving on the socket.
         *
         * @param discoveryTypes discovery types to use
         * @return updated builder
         * @deprecated Use {@link #types(java.util.List)} instead
         */
        @Deprecated(since = "4.0.6", forRemoval = true)
        public Builder discoveryTypes(List discoveryTypes) {
            return types(discoveryTypes);
        }

        /**
         * Adds a discovery type for requested URI discovery for requests arriving on the socket.
         *
         * @param discoveryType the {@link RequestedUriDiscoveryContext.RequestedUriDiscoveryType} to add
         * @return updated builder
         */
        public Builder addDiscoveryType(RequestedUriDiscoveryType discoveryType) {
            discoveryTypes.add(discoveryType);
            return this;
        }

        /**
         * Sets the socket identifier to which the discovery context applies.
         *
         * @param socketId socket identifier (used in logging)
         * @return updated builder
         */
        public Builder socketId(String socketId) {
            this.socketId = socketId;
            return this;
        }

        /**
         * Checks validity of requested URI settings and supplies defaults for omitted settings.
         * 

The behavior of `requested-uri-discovery` config or builder settings can be summarized as follows: *

    *
  • The `requested-uri-discovery` settings are optional.
  • *
  • If `requested-uri-discovery` is absent or is present with `enabled` explicitly set to `false`, Helidon * ignores any {@code Forwarded} or {@code X-Forwarded-*} headers and adopts the * {@code HOST} discovery type. That is, Helidon uses the {@code Host} header for the host * and the request's scheme and port.
  • *
  • If `requested-uri-discovery` is present and enabled, either because {@code enabled} is set to {@code true} * or {@code discoveryTypes} or {@code trusted-proxies} has been set, then Helidon performs a simple validity * check before adopting the selected discovery behavior: If {@code discoveryTypes} is specified and includes * either {@code FORWARDED} or {@code X_FORWARDED} then {@code trusted-proxies} must also be set to at least * one value. Put another way, if requested URI discovery is enabled then {@code trusted-proxies} can be unspecified * only if {@code discoveryTypes} contains only {@code HOST}.
  • *
*

*/ private void prepareAndCheckRequestedUriSettings() { boolean isDiscoveryEnabledDefaulted = (enabled == null); if (enabled == null) { enabled = !discoveryTypes.isEmpty() || trustedProxies != null; } boolean areDiscoveryTypesDefaulted = false; if (enabled) { // Configure a default type if discovery is enabled and no explicit discoveryTypes are configured. if (this.discoveryTypes.isEmpty()) { areDiscoveryTypesDefaulted = true; this.discoveryTypes.add(RequestedUriDiscoveryType.FORWARDED); } // Require _some_ settings for trusted proxies (except for HOST discovery) so the socket does not start unsafely // by accident. The user _can_ set allow.all to run the socket unsafely but at least that way it was // an explicit choice. if (trustedProxies == null && !isDiscoveryTypesOnlyHost()) { throw new UnsafeRequestedUriSettingsException(this, areDiscoveryTypesDefaulted); } } else { // Discovery is disabled so ignore any explicit settings of discovery type and use HOST discovery. if (!discoveryTypes.isEmpty()) { LOGGER.log(System.Logger.Level.INFO, """ Ignoring explicit settings of requested-uri-discovery types and trusted-proxies because requested-uri-discovery.enabled {0} to false """, isDiscoveryEnabledDefaulted ? " defaulted" : "was set"); } discoveryTypes.clear(); discoveryTypes.add(RequestedUriDiscoveryType.HOST); } if (trustedProxies == null) { trustedProxies = AllowList.builder() .addDenied(s -> true) .build(); } } private boolean isDiscoveryTypesOnlyHost() { return discoveryTypes.size() == 1 && discoveryTypes.contains(RequestedUriDiscoveryType.HOST); } private static class RequestedUriDiscoveryContextImpl implements RequestedUriDiscoveryContext { private final boolean enabled; private final List discoveryTypes; private final AllowList trustedProxies; private RequestedUriDiscoveryContextImpl(RequestedUriDiscoveryContext.Builder builder) { this.enabled = builder.enabled; this.discoveryTypes = builder.discoveryTypes; this.trustedProxies = builder.trustedProxies; } @Override public UriInfo uriInfo(String remoteAddress, String localAddress, String requestPath, ServerRequestHeaders headers, UriQuery query, boolean isSecure) { String scheme = null; String authority = null; String host = null; int port = -1; String path = null; // Note: enabled() returns true if discovery is explicitly enabled or if either // requestedUriDiscoveryTypes or trustedProxies is set. if (enabled) { if (trustedProxies.test(hostPart(remoteAddress))) { // Once we discover trusted information using one of the discovery discoveryTypes, we do not mix in // information from other discoveryTypes. nextDiscoveryType: for (var type : discoveryTypes) { switch (type) { case FORWARDED -> { ForwardedDiscovery discovery = discoverUsingForwarded(headers); if (discovery != null) { authority = discovery.authority(); scheme = discovery.scheme(); break nextDiscoveryType; } } case X_FORWARDED -> { XForwardedDiscovery discovery = discoverUsingXForwarded(headers, requestPath); if (discovery != null) { scheme = discovery.scheme(); host = discovery.host(); port = discovery.port(); path = discovery.path(); break nextDiscoveryType; } } case HOST -> { authority = headers.first(HeaderNames.HOST).orElse(null); break nextDiscoveryType; } default -> { authority = headers.first(HeaderNames.HOST).orElse(null); break nextDiscoveryType; } } } } } UriInfo.Builder uriInfo = UriInfo.builder(); // now we must fill values that were not discovered (to have a valid URI information) if (host == null && authority == null) { authority = headers.first(HeaderNames.HOST).orElse(null); } uriInfo.path(path == null ? requestPath : path); uriInfo.host(localAddress); // this is the fallback if (authority != null) { uriInfo.authority(authority); // this is the second possibility } if (host != null) { uriInfo.host(host); // and this one has priority } if (port != -1) { uriInfo.port(port); } /* Discover final values to be used */ if (scheme == null) { if (port == 80) { scheme = "http"; } else if (port == 443) { scheme = "https"; } else { scheme = isSecure ? "https" : "http"; } } uriInfo.scheme(scheme); uriInfo.query(query); return uriInfo.build(); } private static String hostPart(String address) { int colon = address.indexOf(':'); return colon == -1 ? address : address.substring(0, colon); } private ForwardedDiscovery discoverUsingForwarded(ServerRequestHeaders headers) { String scheme = null; String authority = null; List forwardedList = Forwarded.create(headers); if (!forwardedList.isEmpty()) { for (int i = forwardedList.size() - 1; i >= 0; i--) { Forwarded f = forwardedList.get(i); // Because we remained in the loop, the Forwarded entry we are looking at is trustworthy. if (scheme == null && f.proto().isPresent()) { scheme = f.proto().get(); } if (authority == null && f.host().isPresent()) { authority = f.host().get(); } if (f.forClient().isPresent() && !trustedProxies.test(f.forClient().get()) || scheme != null && authority != null) { // This is the first Forwarded entry we've found for which the "for" value is untrusted (and // therefore the proxy which created this Forwarded entry is the most remote trusted one) // OR // we have already harvested the values we need from trusted proxies. // Either way, we do not need to look at further Forwarded entries. break; } } } return authority != null ? new ForwardedDiscovery(authority, scheme) : null; } private XForwardedDiscovery discoverUsingXForwarded(ServerRequestHeaders headers, String requestPath) { // With X-Forwarded-* headers, the X-Forwarded-Host and X-Forwarded-Proto headers appear only once, indicating // the host and protocol supposedly requested by the original client as seen by the proxy which received the // original request. To trust those single values, we need to trust all the X-Forwarded-For instances except // the very first one (the original client itself). boolean discovered = false; String scheme = null; String host = null; int port = -1; String path = null; List xForwardedFors = headers.values(HeaderNames.X_FORWARDED_FOR); boolean areProxiesTrusted = true; if (xForwardedFors.size() > 0) { // Intentionally skip the first X-Forwarded-For value. That is the originating client, and as such it // is not a proxy and we do not need to check its trustworthiness. for (int i = 1; i < xForwardedFors.size(); i++) { areProxiesTrusted &= trustedProxies.test(xForwardedFors.get(i)); } } if (areProxiesTrusted) { scheme = headers.first(HeaderNames.X_FORWARDED_PROTO).orElse(null); host = headers.first(HeaderNames.X_FORWARDED_HOST).orElse(null); port = headers.first(HeaderNames.X_FORWARDED_PORT).map(Integer::parseInt).orElse(-1); path = headers.first(HeaderNames.X_FORWARDED_PREFIX) .map(prefix -> { String absolute = requestPath; return prefix + (absolute.startsWith("/") ? "" : "/") + absolute; }) .orElse(null); // at least one header was present discovered = scheme != null || host != null || port != -1 || path != null; } return discovered ? new XForwardedDiscovery(scheme, host, port, path) : null; } private record ForwardedDiscovery(String authority, String scheme) {} private record XForwardedDiscovery(String scheme, String host, int port, String path) {} } } /** * Types of discovery of frontend URI. Defaults to {@link #HOST} when frontend URI discovery is disabled (uses only Host * header and information about current request to determine scheme, host, port, and path). * Defaults to {@link #FORWARDED} when discovery is enabled. Can be explicitly configured on socket configuration builder. */ enum RequestedUriDiscoveryType { /** * The {@code io.helidon.http.Header#FORWARDED} header is used to discover the original requested URI. */ FORWARDED, /** * The * {@code io.helidon.http.Header#X_FORWARDED_PROTO}, * {@code io.helidon.http.Header#X_FORWARDED_HOST}, * {@code io.helidon.http.Header#X_FORWARDED_PORT}, * {@code io.helidon.http.Header#X_FORWARDED_PREFIX} * headers are used to discover the original requested URI. */ X_FORWARDED, /** * This is the default, only the {@code io.helidon.http.Header#HOST} header is used to discover * requested URI. */ HOST } /** * Indicates unsafe settings for a socket's request URI discovery. *

* This exception typically results when the user has enabled requested URI discovery or selected a discovery type * but has not assigned the trusted proxy {@link AllowList}. *

*/ class UnsafeRequestedUriSettingsException extends RuntimeException { /** * Creates a new exception instance. * * @param requestedUriDiscoveryContextBuilder builder for the socket config * @param areDiscoveryTypesDefaulted whether discovery discoveryTypes were defaulted (as opposed to set explicitly) */ UnsafeRequestedUriSettingsException(RequestedUriDiscoveryContext.Builder requestedUriDiscoveryContextBuilder, boolean areDiscoveryTypesDefaulted) { super(String.format(""" Settings which control requested URI discovery for socket %s are unsafe: \ discovery is enabled with types %s to %s but no trusted proxies were set to protect against forgery of headers. \ Server start-up will not continue. \ Please prepare the trusted-proxies allow-list for this socket using 'allow' and/or 'deny' settings. \ If you choose to start unsafely (not recommended), set trusted-proxies.allow.all to 'true'. \ """, requestedUriDiscoveryContextBuilder.socketId, areDiscoveryTypesDefaulted ? "defaulted" : "set", requestedUriDiscoveryContextBuilder.discoveryTypes)); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy