001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
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 */
016package de.cuioss.http.security.core;
017
018import de.cuioss.http.security.exceptions.UrlSecurityException;
019import org.jspecify.annotations.Nullable;
020
021import java.util.Optional;
022import java.util.function.Predicate;
023
024/**
025 * Core functional interface for HTTP security validation.
026 *
027 * <p>This interface defines the contract for validating HTTP components against security threats.
028 * It follows the "String in, String out, throws on violation" pattern consistently across all
029 * implementations, enabling clean functional programming patterns and easy composition.</p>
030 *
031 * <h3>Design Principles</h3>
032 * <ul>
033 *   <li><strong>Functional Interface</strong> - Can be used with lambda expressions and method references</li>
034 *   <li><strong>Fail Secure</strong> - Throws UrlSecurityException on any security violation</li>
035 *   <li><strong>String/throws Pattern</strong> - Simple contract: input string, output string, throws on failure</li>
036 *   <li><strong>Composable</strong> - Multiple validators can be chained or combined</li>
037 *   <li><strong>Thread Safe</strong> - Implementations should be thread-safe for concurrent use</li>
038 * </ul>
039 *
040 * <h3>Usage Examples</h3>
041 * <pre>
042 * // Simple validation
043 * HttpSecurityValidator pathValidator = input -> {
044 *     if (input.contains("../")) {
045 *         throw UrlSecurityException.builder()
046 *             .failureType(UrlSecurityFailureType.PATH_TRAVERSAL_DETECTED)
047 *             .validationType(ValidationType.URL_PATH)
048 *             .originalInput(input)
049 *             .build();
050 *     }
051 *     return input;
052 * };
053 *
054 * // Using with streams
055 * List&lt;String&gt; validPaths = paths.stream()
056 *     .map(pathValidator::validate)
057 *     .collect(Collectors.toList());
058 *
059 * // Composing validators
060 * HttpSecurityValidator compositeValidator = input ->
061 *     secondValidator.validate(firstValidator.validate(input));
062 * </pre>
063 *
064 * <h3>Implementation Guidelines</h3>
065 * <ul>
066 *   <li>Always validate the most dangerous patterns first</li>
067 *   <li>Provide clear, actionable error messages in exceptions</li>
068 *   <li>Consider performance - validators may be called frequently</li>
069 *   <li>Be consistent with null handling (typically reject null inputs)</li>
070 *   <li>Log security violations appropriately without exposing sensitive data</li>
071 * </ul>
072 *
073 * Implements: Task B3 from HTTP verification specification
074 *
075 * @since 1.0
076 * @see UrlSecurityException
077 * @see ValidationType
078 */
079@FunctionalInterface
080public interface HttpSecurityValidator {
081
082    /**
083     * Validates the input string and returns the sanitized/normalized version.
084     *
085     * <p>This method should examine the input for security violations and either:
086     * <ul>
087     *   <li>Return the input unchanged if it's safe</li>
088     *   <li>Return a sanitized/normalized version if safe transformations are possible</li>
089     *   <li>Return Optional.empty() if the input was null</li>
090     *   <li>Throw UrlSecurityException if the input represents a security threat</li>
091     * </ul>
092     *
093     * <p>The decision between sanitization and rejection depends on the specific validator
094     * and security requirements. Critical security validators should prefer rejection
095     * over sanitization to avoid bypasses.</p>
096     *
097     * @param value The input to validate. May be null.
098     * @return The validated, potentially sanitized or normalized value wrapped in Optional.
099     *         Returns Optional.empty() if the input was null.
100     * @throws UrlSecurityException If the input represents a security violation that cannot
101     *         be safely sanitized. The exception should include detailed context about the
102     *         failure for logging and debugging purposes.
103     * @throws IllegalArgumentException If the input is malformed in a way that prevents
104     *         security analysis (distinct from security violations).
105     */
106    Optional<String> validate(@Nullable String value) throws UrlSecurityException;
107
108    /**
109     * Creates a composite validator that applies this validator followed by the given validator.
110     *
111     * <p>This is a convenience method for chaining validators. The resulting validator will:
112     * <ol>
113     *   <li>Apply this validator to the input</li>
114     *   <li>Apply the {@code after} validator to the result</li>
115     *   <li>Return the final result</li>
116     * </ol>
117     *
118     * <p>If either validator throws an exception, the composition stops and the exception
119     * is propagated.</p>
120     *
121     * @param after The validator to apply after this validator
122     * @return A composite validator that applies both validators in sequence
123     * @throws NullPointerException if {@code after} is null
124     * @since 1.0
125     */
126    @SuppressWarnings({"ConstantConditions", "java:S2583"})
127    default HttpSecurityValidator andThen(HttpSecurityValidator after) {
128        // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages
129        if (after == null) {
130            throw new NullPointerException("after validator must not be null");
131        }
132        return value -> this.validate(value).flatMap(after::validate);
133    }
134
135    /**
136     * Creates a composite validator that applies the given validator followed by this validator.
137     *
138     * <p>This is a convenience method for chaining validators. The resulting validator will:
139     * <ol>
140     *   <li>Apply the {@code before} validator to the input</li>
141     *   <li>Apply this validator to the result</li>
142     *   <li>Return the final result</li>
143     * </ol>
144     *
145     * @param before The validator to apply before this validator
146     * @return A composite validator that applies both validators in sequence
147     * @throws NullPointerException if {@code before} is null
148     * @since 1.0
149     */
150    @SuppressWarnings({"ConstantConditions", "java:S2583"})
151    default HttpSecurityValidator compose(HttpSecurityValidator before) {
152        // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages
153        if (before == null) {
154            throw new NullPointerException("before validator must not be null");
155        }
156        return value -> before.validate(value).flatMap(this::validate);
157    }
158
159    /**
160     * Creates a validator that applies this validator only if the given predicate is true.
161     * If the predicate is false, the input is returned unchanged.
162     *
163     * <p>This is useful for conditional validation based on input characteristics:</p>
164     * <pre>
165     * // Only validate non-empty strings
166     * HttpSecurityValidator conditionalValidator = validator.when(s -> s != null &amp;&amp; !s.isEmpty());
167     *
168     * // Only validate strings that look like URLs
169     * HttpSecurityValidator urlValidator = validator.when(s -> s.startsWith("http"));
170     * </pre>
171     *
172     * @param predicate The condition under which to apply this validator
173     * @return A conditional validator that only applies this validator when the predicate is true
174     * @throws NullPointerException if {@code predicate} is null
175     * @since 1.0
176     */
177    @SuppressWarnings({"ConstantConditions", "java:S2583"})
178    default HttpSecurityValidator when(Predicate<String> predicate) {
179        // Explicit null check despite @NullMarked for fail-fast behavior and clear error messages
180        if (predicate == null) {
181            throw new NullPointerException("predicate must not be null");
182        }
183        return value -> {
184            if (value == null || !predicate.test(value)) {
185                return Optional.ofNullable(value);
186            }
187            return this.validate(value);
188        };
189    }
190
191    /**
192     * Creates an identity validator that always returns the input unchanged.
193     * This is useful as a no-op validator or as a starting point for composition.
194     *
195     * @return An identity validator that performs no validation
196     * @since 1.0
197     */
198    static HttpSecurityValidator identity() {
199        return Optional::ofNullable;
200    }
201
202    /**
203     * Creates a validator that always rejects input with the specified failure type.
204     * This is useful for creating validators that unconditionally block certain inputs.
205     *
206     * @param failureType The failure type to use in the exception
207     * @param validationType The validation type context
208     * @return A validator that always throws UrlSecurityException
209     * @throws NullPointerException if either parameter is null
210     * @since 1.0
211     */
212    @SuppressWarnings({"ConstantConditions", "java:S2583"})
213    static HttpSecurityValidator reject(UrlSecurityFailureType failureType, ValidationType validationType) {
214        // Explicit null checks despite @NullMarked for fail-fast behavior and clear error messages
215        if (failureType == null) {
216            throw new NullPointerException("failureType must not be null");
217        }
218        if (validationType == null) {
219            throw new NullPointerException("validationType must not be null");
220        }
221        return value -> {
222            throw UrlSecurityException.builder()
223                    .failureType(failureType)
224                    .validationType(validationType)
225                    .originalInput(value != null ? value : "null")
226                    .detail("Input unconditionally rejected")
227                    .build();
228        };
229    }
230}