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}