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.config;
017
018import org.jspecify.annotations.Nullable;
019
020import java.util.HashSet;
021import java.util.Objects;
022import java.util.Set;
023
024/**
025 * Builder class for constructing {@link SecurityConfiguration} instances with fluent API.
026 *
027 * <p>This builder provides a convenient way to construct SecurityConfiguration objects
028 * with sensible defaults while allowing fine-grained control over all security settings.
029 * The builder follows the standard builder pattern with method chaining.</p>
030 *
031 * <h3>Design Principles</h3>
032 * <ul>
033 *   <li><strong>Fluent API</strong> - All setter methods return the builder for chaining</li>
034 *   <li><strong>Sensible Defaults</strong> - Pre-configured with reasonable security defaults</li>
035 *   <li><strong>Validation</strong> - Input validation on all parameters</li>
036 *   <li><strong>Immutability</strong> - Produces immutable SecurityConfiguration instances</li>
037 * </ul>
038 *
039 * <h3>Usage Examples</h3>
040 * <pre>
041 * // Basic configuration with defaults
042 * SecurityConfiguration config = SecurityConfiguration.builder().build();
043 *
044 * // Custom configuration
045 * SecurityConfiguration custom = SecurityConfiguration.builder()
046 *     .maxPathLength(2048)
047 *     .allowPathTraversal(false)
048 *     .maxParameterCount(50)
049 *     .requireSecureCookies(true)
050 *     .addBlockedHeaderName("X-Debug")
051 *     .addAllowedContentType("application/json")
052 *     .addAllowedContentType("text/plain")
053 *     .build();
054 *
055 * // Chain multiple settings
056 * SecurityConfiguration strict = SecurityConfiguration.builder()
057 *     .pathSecurity(1024, false)
058 *     .cookieSecurity(true, true, 10, 64, 512)
059 *     .bodySecurity(1024 * 1024, Set.of("application/json"))
060 *     .encoding(false, false, false, true)
061 *     .policies(true, true, true)
062 *     .build();
063 * </pre>
064 *
065 * <h3>Default Values</h3>
066 * <p>The builder is initialized with balanced default values that provide reasonable
067 * security without being overly restrictive:</p>
068 * <ul>
069 *   <li>Path length: 4096 characters</li>
070 *   <li>Parameter count: 100</li>
071 *   <li>Header count: 50</li>
072 *   <li>Cookie count: 20</li>
073 *   <li>Body size: 5MB</li>
074 *   <li>Path traversal: disabled</li>
075 *   <li>Cookie security flags: recommended but not required</li>
076 * </ul>
077 *
078 * Implements: Task C2 from HTTP verification specification
079 *
080 * @since 1.0
081 * @see SecurityConfiguration
082 */
083public class SecurityConfigurationBuilder {
084
085    // Path Security defaults
086    private int maxPathLength = 4096;
087    private boolean allowPathTraversal = false;
088    private boolean allowDoubleEncoding = false;
089
090    // Parameter Security defaults
091    private int maxParameterCount = 100;
092    private int maxParameterNameLength = 128;
093    private int maxParameterValueLength = 2048;
094
095    // Header Security defaults
096    private int maxHeaderCount = 50;
097    private int maxHeaderNameLength = 128;
098    private int maxHeaderValueLength = 2048;
099    private @Nullable Set<String> allowedHeaderNames = null;
100    private Set<String> blockedHeaderNames = new HashSet<>();
101
102    // Cookie Security defaults
103    private int maxCookieCount = 20;
104    private int maxCookieNameLength = 128;
105    private int maxCookieValueLength = 2048;
106    private boolean requireSecureCookies = false;
107    private boolean requireHttpOnlyCookies = false;
108
109    // Body Security defaults
110    private long maxBodySize = 5L * 1024 * 1024; // 5MB
111    private @Nullable Set<String> allowedContentTypes = null;
112    private Set<String> blockedContentTypes = new HashSet<>();
113
114    // Encoding Security defaults
115    private boolean allowNullBytes = false;
116    private boolean allowControlCharacters = false;
117    private boolean allowExtendedAscii = true;
118    private boolean normalizeUnicode = false;
119
120    // General Policy defaults
121    private boolean caseSensitiveComparison = false;
122    private boolean failOnSuspiciousPatterns = false;
123    private boolean logSecurityViolations = true;
124
125    /**
126     * Package-private constructor for internal use.
127     */
128    SecurityConfigurationBuilder() {
129        // Initialize with default values already set above
130    }
131
132    // === Path Security Methods ===
133
134    /**
135     * Sets the maximum allowed path length.
136     *
137     * @param maxLength Maximum path length in characters (must be positive)
138     * @return This builder for method chaining
139     * @throws IllegalArgumentException if maxLength is not positive
140     */
141    public SecurityConfigurationBuilder maxPathLength(int maxLength) {
142        if (maxLength <= 0) {
143            throw new IllegalArgumentException("maxPathLength must be positive, got: " + maxLength);
144        }
145        this.maxPathLength = maxLength;
146        return this;
147    }
148
149    /**
150     * Sets whether path traversal patterns (../) are allowed.
151     *
152     * @param allow true to allow path traversal, false to block it
153     * @return This builder for method chaining
154     */
155    public SecurityConfigurationBuilder allowPathTraversal(boolean allow) {
156        this.allowPathTraversal = allow;
157        return this;
158    }
159
160    /**
161     * Sets whether double URL encoding is allowed.
162     *
163     * @param allow true to allow double encoding, false to block it
164     * @return This builder for method chaining
165     */
166    public SecurityConfigurationBuilder allowDoubleEncoding(boolean allow) {
167        this.allowDoubleEncoding = allow;
168        return this;
169    }
170
171    /**
172     * Configures path security settings in one call.
173     *
174     * @param maxLength Maximum path length
175     * @param allowTraversal Whether to allow path traversal
176     * @return This builder for method chaining
177     */
178    public SecurityConfigurationBuilder pathSecurity(int maxLength, boolean allowTraversal) {
179        return maxPathLength(maxLength).allowPathTraversal(allowTraversal);
180    }
181
182    // === Parameter Security Methods ===
183
184    /**
185     * Sets the maximum number of query parameters allowed.
186     *
187     * @param maxCount Maximum parameter count (must be non-negative)
188     * @return This builder for method chaining
189     * @throws IllegalArgumentException if maxCount is negative
190     */
191    public SecurityConfigurationBuilder maxParameterCount(int maxCount) {
192        if (maxCount < 0) {
193            throw new IllegalArgumentException("maxParameterCount must be non-negative, got: " + maxCount);
194        }
195        this.maxParameterCount = maxCount;
196        return this;
197    }
198
199    /**
200     * Sets the maximum length for parameter names.
201     *
202     * @param maxLength Maximum name length (must be positive)
203     * @return This builder for method chaining
204     * @throws IllegalArgumentException if maxLength is not positive
205     */
206    public SecurityConfigurationBuilder maxParameterNameLength(int maxLength) {
207        if (maxLength <= 0) {
208            throw new IllegalArgumentException("maxParameterNameLength must be positive, got: " + maxLength);
209        }
210        this.maxParameterNameLength = maxLength;
211        return this;
212    }
213
214    /**
215     * Sets the maximum length for parameter values.
216     *
217     * @param maxLength Maximum value length (must be positive)
218     * @return This builder for method chaining
219     * @throws IllegalArgumentException if maxLength is not positive
220     */
221    public SecurityConfigurationBuilder maxParameterValueLength(int maxLength) {
222        if (maxLength <= 0) {
223            throw new IllegalArgumentException("maxParameterValueLength must be positive, got: " + maxLength);
224        }
225        this.maxParameterValueLength = maxLength;
226        return this;
227    }
228
229    /**
230     * Configures parameter security settings in one call.
231     *
232     * @param maxCount Maximum parameter count
233     * @param maxNameLength Maximum parameter name length
234     * @param maxValueLength Maximum parameter value length
235     * @return This builder for method chaining
236     */
237    public SecurityConfigurationBuilder parameterSecurity(int maxCount, int maxNameLength, int maxValueLength) {
238        return maxParameterCount(maxCount)
239                .maxParameterNameLength(maxNameLength)
240                .maxParameterValueLength(maxValueLength);
241    }
242
243    // === Header Security Methods ===
244
245    /**
246     * Sets the maximum number of HTTP headers allowed.
247     *
248     * @param maxCount Maximum header count (must be non-negative)
249     * @return This builder for method chaining
250     * @throws IllegalArgumentException if maxCount is negative
251     */
252    public SecurityConfigurationBuilder maxHeaderCount(int maxCount) {
253        if (maxCount < 0) {
254            throw new IllegalArgumentException("maxHeaderCount must be non-negative, got: " + maxCount);
255        }
256        this.maxHeaderCount = maxCount;
257        return this;
258    }
259
260    /**
261     * Sets the maximum length for header names.
262     *
263     * @param maxLength Maximum name length (must be positive)
264     * @return This builder for method chaining
265     * @throws IllegalArgumentException if maxLength is not positive
266     */
267    public SecurityConfigurationBuilder maxHeaderNameLength(int maxLength) {
268        if (maxLength <= 0) {
269            throw new IllegalArgumentException("maxHeaderNameLength must be positive, got: " + maxLength);
270        }
271        this.maxHeaderNameLength = maxLength;
272        return this;
273    }
274
275    /**
276     * Sets the maximum length for header values.
277     *
278     * @param maxLength Maximum value length (must be positive)
279     * @return This builder for method chaining
280     * @throws IllegalArgumentException if maxLength is not positive
281     */
282    public SecurityConfigurationBuilder maxHeaderValueLength(int maxLength) {
283        if (maxLength <= 0) {
284            throw new IllegalArgumentException("maxHeaderValueLength must be positive, got: " + maxLength);
285        }
286        this.maxHeaderValueLength = maxLength;
287        return this;
288    }
289
290    /**
291     * Adds a header name to the allowed list. If the allowed list is null,
292     * this method initializes it with the given header name.
293     *
294     * @param headerName Header name to allow (must not be null)
295     * @return This builder for method chaining
296     * @throws NullPointerException if headerName is null
297     */
298    public SecurityConfigurationBuilder addAllowedHeaderName(String headerName) {
299        Objects.requireNonNull(headerName, "headerName must not be null");
300        if (allowedHeaderNames == null) {
301            allowedHeaderNames = new HashSet<>();
302        }
303        allowedHeaderNames.add(headerName);
304        return this;
305    }
306
307    /**
308     * Sets the complete list of allowed header names.
309     *
310     * @param headerNames Set of allowed header names (null means all allowed)
311     * @return This builder for method chaining
312     */
313    public SecurityConfigurationBuilder allowedHeaderNames(@Nullable Set<String> headerNames) {
314        this.allowedHeaderNames = headerNames != null ? new HashSet<>(headerNames) : null;
315        return this;
316    }
317
318    /**
319     * Adds a header name to the blocked list.
320     *
321     * @param headerName Header name to block (must not be null)
322     * @return This builder for method chaining
323     * @throws NullPointerException if headerName is null
324     */
325    public SecurityConfigurationBuilder addBlockedHeaderName(String headerName) {
326        Objects.requireNonNull(headerName, "headerName must not be null");
327        blockedHeaderNames.add(headerName);
328        return this;
329    }
330
331    /**
332     * Sets the complete list of blocked header names.
333     *
334     * @param headerNames Set of blocked header names (must not be null)
335     * @return This builder for method chaining
336     * @throws NullPointerException if headerNames is null
337     */
338    public SecurityConfigurationBuilder blockedHeaderNames(Set<String> headerNames) {
339        Objects.requireNonNull(headerNames, "headerNames must not be null");
340        this.blockedHeaderNames = new HashSet<>(headerNames);
341        return this;
342    }
343
344    /**
345     * Configures header security settings in one call.
346     *
347     * @param maxCount Maximum header count
348     * @param maxNameLength Maximum header name length
349     * @param maxValueLength Maximum header value length
350     * @return This builder for method chaining
351     */
352    public SecurityConfigurationBuilder headerSecurity(int maxCount, int maxNameLength, int maxValueLength) {
353        return maxHeaderCount(maxCount)
354                .maxHeaderNameLength(maxNameLength)
355                .maxHeaderValueLength(maxValueLength);
356    }
357
358    // === Cookie Security Methods ===
359
360    /**
361     * Sets the maximum number of cookies allowed.
362     *
363     * @param maxCount Maximum cookie count (must be non-negative)
364     * @return This builder for method chaining
365     * @throws IllegalArgumentException if maxCount is negative
366     */
367    public SecurityConfigurationBuilder maxCookieCount(int maxCount) {
368        if (maxCount < 0) {
369            throw new IllegalArgumentException("maxCookieCount must be non-negative, got: " + maxCount);
370        }
371        this.maxCookieCount = maxCount;
372        return this;
373    }
374
375    /**
376     * Sets the maximum length for cookie names.
377     *
378     * @param maxLength Maximum name length (must be positive)
379     * @return This builder for method chaining
380     * @throws IllegalArgumentException if maxLength is not positive
381     */
382    public SecurityConfigurationBuilder maxCookieNameLength(int maxLength) {
383        if (maxLength <= 0) {
384            throw new IllegalArgumentException("maxCookieNameLength must be positive, got: " + maxLength);
385        }
386        this.maxCookieNameLength = maxLength;
387        return this;
388    }
389
390    /**
391     * Sets the maximum length for cookie values.
392     *
393     * @param maxLength Maximum value length (must be positive)
394     * @return This builder for method chaining
395     * @throws IllegalArgumentException if maxLength is not positive
396     */
397    public SecurityConfigurationBuilder maxCookieValueLength(int maxLength) {
398        if (maxLength <= 0) {
399            throw new IllegalArgumentException("maxCookieValueLength must be positive, got: " + maxLength);
400        }
401        this.maxCookieValueLength = maxLength;
402        return this;
403    }
404
405    /**
406     * Sets whether all cookies must have the Secure flag.
407     *
408     * @param require true to require Secure flag on all cookies
409     * @return This builder for method chaining
410     */
411    public SecurityConfigurationBuilder requireSecureCookies(boolean require) {
412        this.requireSecureCookies = require;
413        return this;
414    }
415
416    /**
417     * Sets whether all cookies must have the HttpOnly flag.
418     *
419     * @param require true to require HttpOnly flag on all cookies
420     * @return This builder for method chaining
421     */
422    public SecurityConfigurationBuilder requireHttpOnlyCookies(boolean require) {
423        this.requireHttpOnlyCookies = require;
424        return this;
425    }
426
427    /**
428     * Configures cookie security settings in one call.
429     *
430     * @param requireSecure Whether to require Secure flag
431     * @param requireHttpOnly Whether to require HttpOnly flag
432     * @param maxCount Maximum cookie count
433     * @param maxNameLength Maximum cookie name length
434     * @param maxValueLength Maximum cookie value length
435     * @return This builder for method chaining
436     */
437    public SecurityConfigurationBuilder cookieSecurity(boolean requireSecure, boolean requireHttpOnly,
438                                                       int maxCount, int maxNameLength, int maxValueLength) {
439        return requireSecureCookies(requireSecure)
440                .requireHttpOnlyCookies(requireHttpOnly)
441                .maxCookieCount(maxCount)
442                .maxCookieNameLength(maxNameLength)
443                .maxCookieValueLength(maxValueLength);
444    }
445
446    // === Body Security Methods ===
447
448    /**
449     * Sets the maximum body size in bytes.
450     *
451     * @param maxSize Maximum body size (must be non-negative)
452     * @return This builder for method chaining
453     * @throws IllegalArgumentException if maxSize is negative
454     */
455    public SecurityConfigurationBuilder maxBodySize(long maxSize) {
456        if (maxSize < 0) {
457            throw new IllegalArgumentException("maxBodySize must be non-negative, got: " + maxSize);
458        }
459        this.maxBodySize = maxSize;
460        return this;
461    }
462
463    /**
464     * Adds a content type to the allowed list. If the allowed list is null,
465     * this method initializes it with the given content type.
466     *
467     * @param contentType Content type to allow (must not be null)
468     * @return This builder for method chaining
469     * @throws NullPointerException if contentType is null
470     */
471    public SecurityConfigurationBuilder addAllowedContentType(String contentType) {
472        Objects.requireNonNull(contentType, "contentType must not be null");
473        if (allowedContentTypes == null) {
474            allowedContentTypes = new HashSet<>();
475        }
476        allowedContentTypes.add(contentType);
477        return this;
478    }
479
480    /**
481     * Sets the complete list of allowed content types.
482     *
483     * @param contentTypes Set of allowed content types (null means all allowed)
484     * @return This builder for method chaining
485     */
486    public SecurityConfigurationBuilder allowedContentTypes(@Nullable Set<String> contentTypes) {
487        this.allowedContentTypes = contentTypes != null ? new HashSet<>(contentTypes) : null;
488        return this;
489    }
490
491    /**
492     * Adds a content type to the blocked list.
493     *
494     * @param contentType Content type to block (must not be null)
495     * @return This builder for method chaining
496     * @throws NullPointerException if contentType is null
497     */
498    public SecurityConfigurationBuilder addBlockedContentType(String contentType) {
499        Objects.requireNonNull(contentType, "contentType must not be null");
500        blockedContentTypes.add(contentType);
501        return this;
502    }
503
504    /**
505     * Sets the complete list of blocked content types.
506     *
507     * @param contentTypes Set of blocked content types (must not be null)
508     * @return This builder for method chaining
509     * @throws NullPointerException if contentTypes is null
510     */
511    public SecurityConfigurationBuilder blockedContentTypes(Set<String> contentTypes) {
512        Objects.requireNonNull(contentTypes, "contentTypes must not be null");
513        this.blockedContentTypes = new HashSet<>(contentTypes);
514        return this;
515    }
516
517    /**
518     * Configures body security settings in one call.
519     *
520     * @param maxSize Maximum body size
521     * @param allowedTypes Set of allowed content types (null = all allowed)
522     * @return This builder for method chaining
523     */
524    public SecurityConfigurationBuilder bodySecurity(long maxSize, @Nullable Set<String> allowedTypes) {
525        return maxBodySize(maxSize).allowedContentTypes(allowedTypes);
526    }
527
528    // === Encoding Security Methods ===
529
530    /**
531     * Sets whether null bytes are allowed in content.
532     *
533     * @param allow true to allow null bytes, false to block them
534     * @return This builder for method chaining
535     */
536    public SecurityConfigurationBuilder allowNullBytes(boolean allow) {
537        this.allowNullBytes = allow;
538        return this;
539    }
540
541    /**
542     * Sets whether control characters are allowed in content.
543     *
544     * @param allow true to allow control characters, false to block them
545     * @return This builder for method chaining
546     */
547    public SecurityConfigurationBuilder allowControlCharacters(boolean allow) {
548        this.allowControlCharacters = allow;
549        return this;
550    }
551
552    /**
553     * Sets whether extended ASCII characters (128-255) are allowed in content.
554     * For URL paths and parameters, this only affects characters 128-255.
555     * For headers and body content, this also enables Unicode support.
556     *
557     * @param allow true to allow extended ASCII and applicable Unicode characters, false to block them
558     * @return This builder for method chaining
559     */
560    public SecurityConfigurationBuilder allowExtendedAscii(boolean allow) {
561        this.allowExtendedAscii = allow;
562        return this;
563    }
564
565
566    /**
567     * Sets whether Unicode normalization should be performed.
568     *
569     * @param normalize true to normalize Unicode, false to leave as-is
570     * @return This builder for method chaining
571     */
572    public SecurityConfigurationBuilder normalizeUnicode(boolean normalize) {
573        this.normalizeUnicode = normalize;
574        return this;
575    }
576
577    /**
578     * Configures encoding security settings in one call.
579     *
580     * @param allowNulls Whether to allow null bytes
581     * @param allowControls Whether to allow control characters
582     * @param allowHighBit Whether to allow high-bit characters
583     * @param normalizeUni Whether to normalize Unicode
584     * @return This builder for method chaining
585     */
586    public SecurityConfigurationBuilder encoding(boolean allowNulls, boolean allowControls,
587                                                 boolean allowHighBit, boolean normalizeUni) {
588        return allowNullBytes(allowNulls)
589                .allowControlCharacters(allowControls)
590                .allowExtendedAscii(allowHighBit)
591                .normalizeUnicode(normalizeUni);
592    }
593
594    // === General Policy Methods ===
595
596    /**
597     * Sets whether string comparisons should be case-sensitive.
598     *
599     * @param caseSensitive true for case-sensitive comparisons
600     * @return This builder for method chaining
601     */
602    public SecurityConfigurationBuilder caseSensitiveComparison(boolean caseSensitive) {
603        this.caseSensitiveComparison = caseSensitive;
604        return this;
605    }
606
607    /**
608     * Sets whether to fail on detection of suspicious patterns.
609     *
610     * @param fail true to fail on suspicious patterns, false to log only
611     * @return This builder for method chaining
612     */
613    public SecurityConfigurationBuilder failOnSuspiciousPatterns(boolean fail) {
614        this.failOnSuspiciousPatterns = fail;
615        return this;
616    }
617
618    /**
619     * Sets whether to log security violations.
620     *
621     * @param log true to enable logging, false to disable
622     * @return This builder for method chaining
623     */
624    public SecurityConfigurationBuilder logSecurityViolations(boolean log) {
625        this.logSecurityViolations = log;
626        return this;
627    }
628
629    /**
630     * Configures general policy settings in one call.
631     *
632     * @param caseSensitive Whether comparisons are case-sensitive
633     * @param failOnSuspicious Whether to fail on suspicious patterns
634     * @param logViolations Whether to log security violations
635     * @return This builder for method chaining
636     */
637    public SecurityConfigurationBuilder policies(boolean caseSensitive, boolean failOnSuspicious, boolean logViolations) {
638        return caseSensitiveComparison(caseSensitive)
639                .failOnSuspiciousPatterns(failOnSuspicious)
640                .logSecurityViolations(logViolations);
641    }
642
643    /**
644     * Builds the SecurityConfiguration with the current settings.
645     *
646     * @return A new immutable SecurityConfiguration instance
647     * @throws IllegalArgumentException if any configuration values are invalid
648     */
649    public SecurityConfiguration build() {
650        return new SecurityConfiguration(
651                maxPathLength, allowPathTraversal, allowDoubleEncoding,
652                maxParameterCount, maxParameterNameLength, maxParameterValueLength,
653                maxHeaderCount, maxHeaderNameLength, maxHeaderValueLength, allowedHeaderNames, blockedHeaderNames,
654                maxCookieCount, maxCookieNameLength, maxCookieValueLength, requireSecureCookies, requireHttpOnlyCookies,
655                maxBodySize, allowedContentTypes, blockedContentTypes,
656                allowNullBytes, allowControlCharacters, allowExtendedAscii, normalizeUnicode,
657                caseSensitiveComparison, failOnSuspiciousPatterns, logSecurityViolations
658        );
659    }
660}