001/*
002 * Copyright 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet;
018
019import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
020import org.jspecify.annotations.NonNull;
021import org.jspecify.annotations.Nullable;
022
023import javax.annotation.concurrent.NotThreadSafe;
024import java.net.InetAddress;
025import java.net.InetSocketAddress;
026import java.util.Map;
027import java.util.Optional;
028import java.util.Set;
029import java.util.function.Predicate;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Resolves a client's effective origin (scheme + host + optional port) from request headers.
035 * <p>
036 * Forwarded headers can be spoofed if Soklet is reachable directly. Choose a {@link TrustPolicy} that matches your
037 * deployment and, for {@link TrustPolicy#TRUST_PROXY_ALLOWLIST}, provide a trusted proxy predicate or allowlist.
038 * If the remote address is missing or not trusted, forwarded headers are ignored.
039 * <p>
040 * Extraction order is: trusted forwarded headers → {@code Host} → (optional) {@code Origin} fallback. {@code Origin}
041 * never overrides a conflicting host value; it only fills missing scheme/port or supplies host when absent.
042 * <p>
043 * Trusted {@code Forwarded host=} and {@code X-Forwarded-Host} values are validated against the same host grammar
044 * Soklet applies to {@code Host}; invalid forwarded host values are ignored.
045 * <p>
046 * Defaults: if {@link #allowOriginFallback(Boolean)} is left unset, {@code Origin} fallback is enabled only for
047 * {@link TrustPolicy#TRUST_ALL}; otherwise it is disabled.
048 */
049@NotThreadSafe
050public final class EffectiveOriginResolver {
051        @NonNull
052        private final Map<@NonNull String, @NonNull Set<@NonNull String>> headers;
053        @NonNull
054        private final TrustPolicy trustPolicy;
055        @Nullable
056        private InetSocketAddress remoteAddress;
057        @Nullable
058        private Predicate<InetSocketAddress> trustedProxyPredicate;
059        @Nullable
060        private Boolean allowOriginFallback;
061
062        /**
063         * Acquires a resolver seeded with raw request headers and a trust policy.
064         *
065         * @param headers     HTTP request headers
066         * @param trustPolicy how forwarded headers should be trusted
067         * @return the resolver
068         */
069        @NonNull
070        public static EffectiveOriginResolver withHeaders(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
071                                                                                                                                                                                                                @NonNull TrustPolicy trustPolicy) {
072                requireNonNull(headers);
073                requireNonNull(trustPolicy);
074                return new EffectiveOriginResolver(headers, trustPolicy);
075        }
076
077        /**
078         * Acquires a resolver seeded with a {@link Request} and a trust policy.
079         *
080         * @param request     the current request
081         * @param trustPolicy how forwarded headers should be trusted
082         * @return the resolver
083         */
084        @NonNull
085        public static EffectiveOriginResolver withRequest(@NonNull Request request,
086                                                                                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
087                requireNonNull(request);
088                EffectiveOriginResolver resolver = withHeaders(request.getHeaders(), trustPolicy);
089                resolver.remoteAddress = request.getRemoteAddress().orElse(null);
090                return resolver;
091        }
092
093        private EffectiveOriginResolver(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
094                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
095                this.headers = new LinkedCaseInsensitiveMap<>(headers);
096                this.trustPolicy = trustPolicy;
097        }
098
099        /**
100         * Resolves the effective origin.
101         *
102         * @return the effective origin, or {@link Optional#empty()} if it could not be determined
103         */
104        @NonNull
105        public Optional<String> resolve() {
106                return Utilities.extractEffectiveOrigin(this);
107        }
108
109        /**
110         * The remote address of the client connection.
111         *
112         * @param remoteAddress the remote address, or {@code null} if unavailable
113         * @return this resolver
114         */
115        @NonNull
116        public EffectiveOriginResolver remoteAddress(@Nullable InetSocketAddress remoteAddress) {
117                this.remoteAddress = remoteAddress;
118                return this;
119        }
120
121        /**
122         * Predicate used when {@link TrustPolicy#TRUST_PROXY_ALLOWLIST} is in effect.
123         *
124         * @param trustedProxyPredicate predicate that returns {@code true} for trusted proxies
125         * @return this resolver
126         */
127        @NonNull
128        public EffectiveOriginResolver trustedProxyPredicate(@Nullable Predicate<InetSocketAddress> trustedProxyPredicate) {
129                this.trustedProxyPredicate = trustedProxyPredicate;
130                return this;
131        }
132
133        /**
134         * Allows specifying an IP allowlist for trusted proxies.
135         *
136         * @param trustedProxyAddresses IP addresses of trusted proxies
137         * @return this resolver
138         */
139        @NonNull
140        public EffectiveOriginResolver trustedProxyAddresses(@NonNull Set<@NonNull InetAddress> trustedProxyAddresses) {
141                requireNonNull(trustedProxyAddresses);
142                Set<InetAddress> normalizedAddresses = Set.copyOf(trustedProxyAddresses);
143                this.trustedProxyPredicate = remoteAddress -> {
144                        if (remoteAddress == null)
145                                return false;
146
147                        InetAddress address = remoteAddress.getAddress();
148                        return address != null && normalizedAddresses.contains(address);
149                };
150                return this;
151        }
152
153        /**
154         * Controls whether {@code Origin} is used as a fallback signal when determining the client URL prefix.
155         *
156         * @param allowOriginFallback {@code true} to allow {@code Origin} fallback, {@code false} to disable it
157         * @return this resolver
158         */
159        @NonNull
160        public EffectiveOriginResolver allowOriginFallback(@Nullable Boolean allowOriginFallback) {
161                this.allowOriginFallback = allowOriginFallback;
162                return this;
163        }
164
165        @NonNull
166        Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() {
167                return this.headers;
168        }
169
170        @NonNull
171        TrustPolicy getTrustPolicy() {
172                return this.trustPolicy;
173        }
174
175        @Nullable
176        InetSocketAddress getRemoteAddress() {
177                return this.remoteAddress;
178        }
179
180        @Nullable
181        Predicate<InetSocketAddress> getTrustedProxyPredicate() {
182                return this.trustedProxyPredicate;
183        }
184
185        @Nullable
186        Boolean getAllowOriginFallback() {
187                return this.allowOriginFallback;
188        }
189
190        /**
191         * Forwarded header trust policy.
192         */
193        public enum TrustPolicy {
194                /**
195                 * Trust forwarded headers from any source.
196                 */
197                TRUST_ALL,
198
199                /**
200                 * Trust forwarded headers only from proxies in a configured allowlist.
201                 */
202                TRUST_PROXY_ALLOWLIST,
203
204                /**
205                 * Ignore forwarded headers entirely.
206                 */
207                TRUST_NONE
208        }
209}