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.EffectiveOriginResolver.TrustPolicy;
020import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
021import org.jspecify.annotations.NonNull;
022import org.jspecify.annotations.Nullable;
023
024import javax.annotation.concurrent.NotThreadSafe;
025import java.net.InetAddress;
026import java.net.InetSocketAddress;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030import java.util.function.Predicate;
031
032import static java.util.Objects.requireNonNull;
033
034/**
035 * Resolves a client's effective IP address from a request's socket peer and forwarded headers.
036 * <p>
037 * Forwarded headers can be spoofed if Soklet is reachable directly. Choose a {@link TrustPolicy} that matches your
038 * deployment and, for {@link TrustPolicy#TRUST_PROXY_ALLOWLIST}, provide a trusted proxy predicate or allowlist.
039 * If the remote address is missing or not trusted, forwarded headers are ignored and the socket peer is returned when available.
040 * <p>
041 * Extraction order is: trusted {@code Forwarded for=} values, trusted {@code X-Forwarded-For} values, then the socket peer.
042 * Only IP literals are accepted from forwarded headers; hostnames, obfuscated identifiers, {@code unknown}, and malformed values are ignored.
043 *
044 * @author <a href="https://www.revetkn.com">Mark Allen</a>
045 */
046@NotThreadSafe
047public final class EffectiveClientIpResolver {
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
057        /**
058         * Acquires a resolver seeded with raw request headers and a trust policy.
059         *
060         * @param headers     HTTP request headers
061         * @param trustPolicy how forwarded headers should be trusted
062         * @return the resolver
063         */
064        @NonNull
065        public static EffectiveClientIpResolver withHeaders(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
066                                                                                                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
067                requireNonNull(headers);
068                requireNonNull(trustPolicy);
069                return new EffectiveClientIpResolver(headers, trustPolicy);
070        }
071
072        /**
073         * Acquires a resolver seeded with a {@link Request} and a trust policy.
074         *
075         * @param request     the current request
076         * @param trustPolicy how forwarded headers should be trusted
077         * @return the resolver
078         */
079        @NonNull
080        public static EffectiveClientIpResolver withRequest(@NonNull Request request,
081                                                                                                                                                                                                                        @NonNull TrustPolicy trustPolicy) {
082                requireNonNull(request);
083                EffectiveClientIpResolver resolver = withHeaders(request.getHeaders(), trustPolicy);
084                resolver.remoteAddress = request.getRemoteAddress().orElse(null);
085                return resolver;
086        }
087
088        private EffectiveClientIpResolver(@NonNull Map<@NonNull String, @NonNull Set<@NonNull String>> headers,
089                                                                                                                                                @NonNull TrustPolicy trustPolicy) {
090                this.headers = new LinkedCaseInsensitiveMap<>(headers);
091                this.trustPolicy = trustPolicy;
092        }
093
094        /**
095         * Resolves the effective client IP address.
096         *
097         * @return the effective client IP address, or {@link Optional#empty()} if no client IP could be determined
098         */
099        @NonNull
100        public Optional<InetAddress> resolve() {
101                return Utilities.extractEffectiveClientIp(this);
102        }
103
104        /**
105         * The remote address of the client connection.
106         *
107         * @param remoteAddress the remote address, or {@code null} if unavailable
108         * @return this resolver
109         */
110        @NonNull
111        public EffectiveClientIpResolver remoteAddress(@Nullable InetSocketAddress remoteAddress) {
112                this.remoteAddress = remoteAddress;
113                return this;
114        }
115
116        /**
117         * Predicate used when {@link TrustPolicy#TRUST_PROXY_ALLOWLIST} is in effect.
118         *
119         * @param trustedProxyPredicate predicate that returns {@code true} for trusted proxies
120         * @return this resolver
121         */
122        @NonNull
123        public EffectiveClientIpResolver trustedProxyPredicate(@Nullable Predicate<InetSocketAddress> trustedProxyPredicate) {
124                this.trustedProxyPredicate = trustedProxyPredicate;
125                return this;
126        }
127
128        /**
129         * Allows specifying an IP allowlist for trusted proxies.
130         *
131         * @param trustedProxyAddresses IP addresses of trusted proxies
132         * @return this resolver
133         */
134        @NonNull
135        public EffectiveClientIpResolver trustedProxyAddresses(@NonNull Set<@NonNull InetAddress> trustedProxyAddresses) {
136                requireNonNull(trustedProxyAddresses);
137                Set<InetAddress> normalizedAddresses = Set.copyOf(trustedProxyAddresses);
138                this.trustedProxyPredicate = remoteAddress -> {
139                        if (remoteAddress == null)
140                                return false;
141
142                        InetAddress address = remoteAddress.getAddress();
143                        return address != null && normalizedAddresses.contains(address);
144                };
145                return this;
146        }
147
148        @NonNull
149        Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() {
150                return this.headers;
151        }
152
153        @NonNull
154        TrustPolicy getTrustPolicy() {
155                return this.trustPolicy;
156        }
157
158        @Nullable
159        InetSocketAddress getRemoteAddress() {
160                return this.remoteAddress;
161        }
162
163        @Nullable
164        Predicate<InetSocketAddress> getTrustedProxyPredicate() {
165                return this.trustedProxyPredicate;
166        }
167}