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 * Defaults: if {@link #allowOriginFallback(Boolean)} is left unset, {@code Origin} fallback is enabled only for
044 * {@link TrustPolicy#TRUST_ALL}; otherwise it is disabled.
045 */
046@NotThreadSafe
047public final class EffectiveOriginResolver {
048        @NonNull
049        private final Map<@NonNull String, @NonNull Set<@NonNull String>> headers;
050        @NonNull
051        private final TrustPolicy trustPolicy;
052        @Nullable
053        private InetSocketAddress remoteAddress;
054        @Nullable
055        private Predicate<InetSocketAddress> trustedProxyPredicate;
056        @Nullable
057        private Boolean allowOriginFallback;
058
059        /**
060         * Acquires a resolver seeded with raw request headers and a trust policy.
061         *
062         * @param headers     HTTP request headers
063         * @param trustPolicy how forwarded headers should be trusted
064         * @return the resolver
065         */
066        @NonNull
067        public static EffectiveOriginResolver withHeaders(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
068                                                                                                                                                                                                                @NonNull TrustPolicy trustPolicy) {
069                requireNonNull(headers);
070                requireNonNull(trustPolicy);
071                return new EffectiveOriginResolver(headers, trustPolicy);
072        }
073
074        /**
075         * Acquires a resolver seeded with a {@link Request} and a trust policy.
076         *
077         * @param request     the current request
078         * @param trustPolicy how forwarded headers should be trusted
079         * @return the resolver
080         */
081        @NonNull
082        public static EffectiveOriginResolver withRequest(@NonNull Request request,
083                                                                                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
084                requireNonNull(request);
085                EffectiveOriginResolver resolver = withHeaders(request.getHeaders(), trustPolicy);
086                resolver.remoteAddress = request.getRemoteAddress().orElse(null);
087                return resolver;
088        }
089
090        private EffectiveOriginResolver(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
091                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
092                this.headers = new LinkedCaseInsensitiveMap<>(headers);
093                this.trustPolicy = trustPolicy;
094        }
095
096        /**
097         * Resolves the effective origin.
098         *
099         * @return the effective origin, or {@link Optional#empty()} if it could not be determined
100         */
101        @NonNull
102        public Optional<String> resolve() {
103                return Utilities.extractEffectiveOrigin(this);
104        }
105
106        /**
107         * The remote address of the client connection.
108         *
109         * @param remoteAddress the remote address, or {@code null} if unavailable
110         * @return this resolver
111         */
112        @NonNull
113        public EffectiveOriginResolver remoteAddress(@Nullable InetSocketAddress remoteAddress) {
114                this.remoteAddress = remoteAddress;
115                return this;
116        }
117
118        /**
119         * Predicate used when {@link TrustPolicy#TRUST_PROXY_ALLOWLIST} is in effect.
120         *
121         * @param trustedProxyPredicate predicate that returns {@code true} for trusted proxies
122         * @return this resolver
123         */
124        @NonNull
125        public EffectiveOriginResolver trustedProxyPredicate(@Nullable Predicate<InetSocketAddress> trustedProxyPredicate) {
126                this.trustedProxyPredicate = trustedProxyPredicate;
127                return this;
128        }
129
130        /**
131         * Allows specifying an IP allowlist for trusted proxies.
132         *
133         * @param trustedProxyAddresses IP addresses of trusted proxies
134         * @return this resolver
135         */
136        @NonNull
137        public EffectiveOriginResolver trustedProxyAddresses(@NonNull Set<@NonNull InetAddress> trustedProxyAddresses) {
138                requireNonNull(trustedProxyAddresses);
139                Set<InetAddress> normalizedAddresses = Set.copyOf(trustedProxyAddresses);
140                this.trustedProxyPredicate = remoteAddress -> {
141                        if (remoteAddress == null)
142                                return false;
143
144                        InetAddress address = remoteAddress.getAddress();
145                        return address != null && normalizedAddresses.contains(address);
146                };
147                return this;
148        }
149
150        /**
151         * Controls whether {@code Origin} is used as a fallback signal when determining the client URL prefix.
152         *
153         * @param allowOriginFallback {@code true} to allow {@code Origin} fallback, {@code false} to disable it
154         * @return this resolver
155         */
156        @NonNull
157        public EffectiveOriginResolver allowOriginFallback(@Nullable Boolean allowOriginFallback) {
158                this.allowOriginFallback = allowOriginFallback;
159                return this;
160        }
161
162        @NonNull
163        Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() {
164                return this.headers;
165        }
166
167        @NonNull
168        TrustPolicy getTrustPolicy() {
169                return this.trustPolicy;
170        }
171
172        @Nullable
173        InetSocketAddress getRemoteAddress() {
174                return this.remoteAddress;
175        }
176
177        @Nullable
178        Predicate<InetSocketAddress> getTrustedProxyPredicate() {
179                return this.trustedProxyPredicate;
180        }
181
182        @Nullable
183        Boolean getAllowOriginFallback() {
184                return this.allowOriginFallback;
185        }
186
187        /**
188         * Forwarded header trust policy.
189         */
190        public enum TrustPolicy {
191                /**
192                 * Trust forwarded headers from any source.
193                 */
194                TRUST_ALL,
195
196                /**
197                 * Trust forwarded headers only from proxies in a configured allowlist.
198                 */
199                TRUST_PROXY_ALLOWLIST,
200
201                /**
202                 * Ignore forwarded headers entirely.
203                 */
204                TRUST_NONE
205        }
206}