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.exceptions;
017
018import de.cuioss.http.security.core.UrlSecurityFailureType;
019import de.cuioss.http.security.core.ValidationType;
020import lombok.Builder;
021import lombok.EqualsAndHashCode;
022import lombok.Getter;
023import org.jspecify.annotations.Nullable;
024
025import java.util.Optional;
026import java.util.regex.Pattern;
027
028/**
029 * Main exception for HTTP security validation failures.
030 * Extends RuntimeException to enable clean functional interface usage and fail-fast behavior.
031 *
032 * <h3>Design Principles</h3>
033 * <ul>
034 *   <li><strong>Fail Secure</strong> - Throws on any security violation for immediate handling</li>
035 *   <li><strong>Rich Context</strong> - Provides detailed failure information for debugging and logging</li>
036 *   <li><strong>Builder Pattern</strong> - Fluent API for exception construction</li>
037 *   <li><strong>Immutable</strong> - All fields are final and thread-safe</li>
038 * </ul>
039 *
040 * <h3>Usage Examples</h3>
041 * <pre>
042 * // Simple security violation
043 * throw UrlSecurityException.builder()
044 *     .failureType(UrlSecurityFailureType.PATH_TRAVERSAL_DETECTED)
045 *     .validationType(ValidationType.URL_PATH)
046 *     .originalInput("../../../etc/passwd")
047 *     .build();
048 *
049 * // Detailed violation with sanitized input
050 * throw UrlSecurityException.builder()
051 *     .failureType(UrlSecurityFailureType.INVALID_CHARACTER)
052 *     .validationType(ValidationType.PARAMETER_VALUE)
053 *     .originalInput("user&lt;script&gt;test(1)&lt;/script&gt;")
054 *     .sanitizedInput("userscripttest1script")
055 *     .detail("Removed script tags and special characters")
056 *     .build();
057 *
058 * // Chained exception
059 * throw UrlSecurityException.builder()
060 *     .failureType(UrlSecurityFailureType.INVALID_ENCODING)
061 *     .validationType(ValidationType.URL_PATH)
062 *     .originalInput("%ZZ%invalid")
063 *     .cause(originalException)
064 *     .build();
065 * </pre>
066 *
067 * Implements: Task B2 from HTTP verification specification
068 *
069 * @since 1.0
070 */
071@EqualsAndHashCode(callSuper = true)
072public class UrlSecurityException extends RuntimeException {
073
074    /**
075     * Pre-compiled pattern for removing control characters from log output.
076     * Matches control characters (0x00-0x1F) and DEL character (0x7F).
077     */
078    private static final Pattern CONTROL_CHARS_PATTERN = Pattern.compile("[\\x00-\\x1F\\x7F]");
079
080    @Getter private final UrlSecurityFailureType failureType;
081    @Getter private final ValidationType validationType;
082    @Getter private final String originalInput;
083    @Nullable private final String sanitizedInput;
084    @Nullable private final String detail;
085
086    /**
087     * Creates a new UrlSecurityException with the specified parameters.
088     * Use the {@link #builder()} method for easier construction.
089     *
090     * @param failureType The type of security failure that occurred
091     * @param validationType The type of HTTP component being validated
092     * @param originalInput The original input that caused the security violation
093     * @param sanitizedInput Optional sanitized version of the input (may be null)
094     * @param detail Optional additional detail about the failure (may be null)
095     * @param cause Optional underlying cause exception (may be null)
096     */
097    @Builder
098    private UrlSecurityException(UrlSecurityFailureType failureType,
099                                 ValidationType validationType,
100                                 String originalInput,
101                                 @Nullable String sanitizedInput,
102                                 @Nullable String detail,
103                                 @Nullable Throwable cause) {
104        super(buildMessage(failureType, validationType, originalInput, detail), cause);
105        this.failureType = failureType;
106        this.validationType = validationType;
107        this.originalInput = originalInput;
108        this.sanitizedInput = sanitizedInput;
109        this.detail = detail;
110    }
111
112    /**
113     * Gets the sanitized version of the input, if available.
114     *
115     * @return The sanitized input wrapped in Optional, or empty if not provided
116     */
117    public Optional<String> getSanitizedInput() {
118        return Optional.ofNullable(sanitizedInput);
119    }
120
121    /**
122     * Gets additional detail about the security failure.
123     *
124     * @return Additional detail wrapped in Optional, or empty if not provided
125     */
126    public Optional<String> getDetail() {
127        return Optional.ofNullable(detail);
128    }
129
130    /**
131     * Builds a comprehensive error message from the exception components.
132     *
133     * @param failureType The type of failure
134     * @param validationType The type of validation
135     * @param originalInput The input that caused the failure
136     * @param detail Optional additional detail
137     * @return A formatted error message
138     */
139    private static String buildMessage(UrlSecurityFailureType failureType,
140                                       ValidationType validationType,
141                                       String originalInput,
142                                       @Nullable String detail) {
143        StringBuilder sb = new StringBuilder();
144        sb.append("Security validation failed [").append(validationType).append("]: ");
145        sb.append(failureType.getDescription());
146
147        if (detail != null && !detail.trim().isEmpty()) {
148            sb.append(" - ").append(detail);
149        }
150
151        // Safely truncate input for logging to prevent log injection
152        String truncatedInput = truncateForLogging(originalInput);
153        sb.append(" (input: '").append(truncatedInput).append("')");
154
155        return sb.toString();
156    }
157
158    /**
159     * Safely truncates input for logging to prevent log injection attacks.
160     *
161     * @param input The input to truncate
162     * @return Safe truncated input
163     */
164    private static String truncateForLogging(@Nullable String input) {
165        if (input == null) {
166            return "null";
167        }
168
169        // Remove control characters and limit length
170        String safe = CONTROL_CHARS_PATTERN.matcher(input).replaceAll("?");
171
172        if (safe.length() > 200) {
173            return safe.substring(0, 200) + "...";
174        }
175
176        return safe;
177    }
178
179
180    @Override
181    public String toString() {
182        return getClass().getSimpleName() + "{" +
183                "failureType=" + failureType +
184                ", validationType=" + validationType +
185                ", originalInput='" + truncateForLogging(originalInput) + '\'' +
186                ", sanitizedInput='" + (sanitizedInput != null ? truncateForLogging(sanitizedInput) : null) + '\'' +
187                ", detail='" + detail + '\'' +
188                ", cause=" + getCause() +
189                '}';
190    }
191
192}