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.Objects; 021import java.util.Set; 022import java.util.stream.Collectors; 023 024/** 025 * Immutable class representing comprehensive security configuration for HTTP validation. 026 * 027 * <p>This class encapsulates all security policies and settings needed to configure 028 * HTTP security validators. It provides a type-safe, immutable configuration object 029 * that can be shared across multiple validation operations.</p> 030 * 031 * <h3>Design Principles</h3> 032 * <ul> 033 * <li><strong>Immutability</strong> - Configuration cannot be modified once created</li> 034 * <li><strong>Type Safety</strong> - Strongly typed configuration parameters</li> 035 * <li><strong>Completeness</strong> - Covers all aspects of HTTP security validation</li> 036 * <li><strong>Composability</strong> - Easy to combine with builder patterns</li> 037 * <li><strong>Performance</strong> - Pre-processes sets for O(1) case-insensitive lookups</li> 038 * </ul> 039 * 040 * <h3>Configuration Categories</h3> 041 * <ul> 042 * <li><strong>Path Security</strong> - Path traversal prevention, allowed patterns</li> 043 * <li><strong>Parameter Security</strong> - Query parameter validation rules</li> 044 * <li><strong>Header Security</strong> - HTTP header validation policies</li> 045 * <li><strong>Cookie Security</strong> - Cookie validation and security requirements</li> 046 * <li><strong>Body Security</strong> - Request/response body validation settings</li> 047 * <li><strong>Encoding Security</strong> - URL encoding and character validation</li> 048 * <li><strong>Length Limits</strong> - Size restrictions for various HTTP components</li> 049 * <li><strong>General Policies</strong> - Cross-cutting security concerns</li> 050 * </ul> 051 * 052 * <h3>Usage Examples</h3> 053 * <pre> 054 * // Create with builder 055 * SecurityConfiguration config = SecurityConfiguration.builder() 056 * .maxPathLength(2048) 057 * .allowPathTraversal(false) 058 * .maxParameterCount(100) 059 * .requireSecureCookies(true) 060 * .build(); 061 * 062 * // Use in validation 063 * PathValidator validator = new PathValidator(config); 064 * validator.validate("/api/users/123"); 065 * 066 * // Create restrictive configuration 067 * SecurityConfiguration strict = SecurityConfiguration.strict(); 068 * 069 * // Create permissive configuration 070 * SecurityConfiguration lenient = SecurityConfiguration.lenient(); 071 * </pre> 072 * 073 * Implements: Task C1 from HTTP verification specification 074 * 075 * @since 1.0 076 * @see SecurityConfigurationBuilder 077 */ 078@SuppressWarnings("javaarchitecture:S7027") 079public final class SecurityConfiguration { 080 081 // Path Security 082 private final int maxPathLength; 083 private final boolean allowPathTraversal; 084 private final boolean allowDoubleEncoding; 085 086 // Parameter Security 087 private final int maxParameterCount; 088 private final int maxParameterNameLength; 089 private final int maxParameterValueLength; 090 091 // Header Security 092 private final int maxHeaderCount; 093 private final int maxHeaderNameLength; 094 private final int maxHeaderValueLength; 095 private final @Nullable Set<String> allowedHeaderNames; 096 private final Set<String> blockedHeaderNames; 097 098 // Cookie Security 099 private final int maxCookieCount; 100 private final int maxCookieNameLength; 101 private final int maxCookieValueLength; 102 private final boolean requireSecureCookies; 103 private final boolean requireHttpOnlyCookies; 104 105 // Body Security 106 private final long maxBodySize; 107 private final @Nullable Set<String> allowedContentTypes; 108 private final Set<String> blockedContentTypes; 109 110 // Encoding Security 111 private final boolean allowNullBytes; 112 private final boolean allowControlCharacters; 113 private final boolean allowExtendedAscii; 114 private final boolean normalizeUnicode; 115 116 // General Policies 117 private final boolean caseSensitiveComparison; 118 private final boolean failOnSuspiciousPatterns; 119 private final boolean logSecurityViolations; 120 121 // Pre-processed lowercase sets for O(1) case-insensitive lookups 122 // Only populated when caseSensitiveComparison is false 123 private final @Nullable Set<String> allowedHeaderNamesLowercase; 124 private final Set<String> blockedHeaderNamesLowercase; 125 private final @Nullable Set<String> allowedContentTypesLowercase; 126 private final Set<String> blockedContentTypesLowercase; 127 128 /** 129 * Creates a SecurityConfiguration with validation of constraints. 130 */ 131 @SuppressWarnings({"java:S107", "java:S3776"}) // S107: Constructor has many parameters by design - using Builder pattern for construction 132 // S3776: Cognitive complexity is from necessary validation in security-critical code 133 SecurityConfiguration( 134 int maxPathLength, 135 boolean allowPathTraversal, 136 boolean allowDoubleEncoding, 137 int maxParameterCount, 138 int maxParameterNameLength, 139 int maxParameterValueLength, 140 int maxHeaderCount, 141 int maxHeaderNameLength, 142 int maxHeaderValueLength, 143 @Nullable Set<String> allowedHeaderNames, 144 Set<String> blockedHeaderNames, 145 int maxCookieCount, 146 int maxCookieNameLength, 147 int maxCookieValueLength, 148 boolean requireSecureCookies, 149 boolean requireHttpOnlyCookies, 150 long maxBodySize, 151 @Nullable Set<String> allowedContentTypes, 152 Set<String> blockedContentTypes, 153 boolean allowNullBytes, 154 boolean allowControlCharacters, 155 boolean allowExtendedAscii, 156 boolean normalizeUnicode, 157 boolean caseSensitiveComparison, 158 boolean failOnSuspiciousPatterns, 159 boolean logSecurityViolations) { 160 161 // Validate length limits are positive 162 if (maxPathLength <= 0) { 163 throw new IllegalArgumentException("maxPathLength must be positive, got: " + maxPathLength); 164 } 165 if (maxParameterCount < 0) { 166 throw new IllegalArgumentException("maxParameterCount must be non-negative, got: " + maxParameterCount); 167 } 168 if (maxParameterNameLength <= 0) { 169 throw new IllegalArgumentException("maxParameterNameLength must be positive, got: " + maxParameterNameLength); 170 } 171 if (maxParameterValueLength <= 0) { 172 throw new IllegalArgumentException("maxParameterValueLength must be positive, got: " + maxParameterValueLength); 173 } 174 if (maxHeaderCount < 0) { 175 throw new IllegalArgumentException("maxHeaderCount must be non-negative, got: " + maxHeaderCount); 176 } 177 if (maxHeaderNameLength <= 0) { 178 throw new IllegalArgumentException("maxHeaderNameLength must be positive, got: " + maxHeaderNameLength); 179 } 180 if (maxHeaderValueLength <= 0) { 181 throw new IllegalArgumentException("maxHeaderValueLength must be positive, got: " + maxHeaderValueLength); 182 } 183 if (maxCookieCount < 0) { 184 throw new IllegalArgumentException("maxCookieCount must be non-negative, got: " + maxCookieCount); 185 } 186 if (maxCookieNameLength <= 0) { 187 throw new IllegalArgumentException("maxCookieNameLength must be positive, got: " + maxCookieNameLength); 188 } 189 if (maxCookieValueLength <= 0) { 190 throw new IllegalArgumentException("maxCookieValueLength must be positive, got: " + maxCookieValueLength); 191 } 192 if (maxBodySize < 0) { 193 throw new IllegalArgumentException("maxBodySize must be non-negative, got: " + maxBodySize); 194 } 195 196 // Assign final fields 197 this.maxPathLength = maxPathLength; 198 this.allowPathTraversal = allowPathTraversal; 199 this.allowDoubleEncoding = allowDoubleEncoding; 200 this.maxParameterCount = maxParameterCount; 201 this.maxParameterNameLength = maxParameterNameLength; 202 this.maxParameterValueLength = maxParameterValueLength; 203 this.maxHeaderCount = maxHeaderCount; 204 this.maxHeaderNameLength = maxHeaderNameLength; 205 this.maxHeaderValueLength = maxHeaderValueLength; 206 this.maxCookieCount = maxCookieCount; 207 this.maxCookieNameLength = maxCookieNameLength; 208 this.maxCookieValueLength = maxCookieValueLength; 209 this.requireSecureCookies = requireSecureCookies; 210 this.requireHttpOnlyCookies = requireHttpOnlyCookies; 211 this.maxBodySize = maxBodySize; 212 this.allowNullBytes = allowNullBytes; 213 this.allowControlCharacters = allowControlCharacters; 214 this.allowExtendedAscii = allowExtendedAscii; 215 this.normalizeUnicode = normalizeUnicode; 216 this.caseSensitiveComparison = caseSensitiveComparison; 217 this.failOnSuspiciousPatterns = failOnSuspiciousPatterns; 218 this.logSecurityViolations = logSecurityViolations; 219 220 // Ensure sets are immutable and non-null 221 this.allowedHeaderNames = allowedHeaderNames != null ? 222 Set.copyOf(allowedHeaderNames) : null; 223 this.blockedHeaderNames = Set.copyOf(blockedHeaderNames); 224 this.allowedContentTypes = allowedContentTypes != null ? 225 Set.copyOf(allowedContentTypes) : null; 226 this.blockedContentTypes = Set.copyOf(blockedContentTypes); 227 228 // Pre-process sets for case-insensitive comparison if needed 229 // This optimization changes lookups from O(n) to O(1) average case 230 if (!caseSensitiveComparison) { 231 // Convert all strings to lowercase for efficient case-insensitive lookups 232 this.allowedHeaderNamesLowercase = this.allowedHeaderNames != null ? 233 this.allowedHeaderNames.stream() 234 .map(String::toLowerCase) 235 .collect(Collectors.toUnmodifiableSet()) : null; 236 this.blockedHeaderNamesLowercase = this.blockedHeaderNames.stream() 237 .map(String::toLowerCase) 238 .collect(Collectors.toUnmodifiableSet()); 239 this.allowedContentTypesLowercase = this.allowedContentTypes != null ? 240 this.allowedContentTypes.stream() 241 .map(String::toLowerCase) 242 .collect(Collectors.toUnmodifiableSet()) : null; 243 this.blockedContentTypesLowercase = this.blockedContentTypes.stream() 244 .map(String::toLowerCase) 245 .collect(Collectors.toUnmodifiableSet()); 246 } else { 247 // Not needed for case-sensitive comparison 248 this.allowedHeaderNamesLowercase = null; 249 this.blockedHeaderNamesLowercase = Set.of(); 250 this.allowedContentTypesLowercase = null; 251 this.blockedContentTypesLowercase = Set.of(); 252 } 253 } 254 255 /** 256 * Creates a builder for constructing SecurityConfiguration instances. 257 * 258 * @return A new SecurityConfigurationBuilder with default values 259 */ 260 public static SecurityConfigurationBuilder builder() { 261 return new SecurityConfigurationBuilder(); 262 } 263 264 /** 265 * Creates a strict security configuration with tight restrictions. 266 * This configuration prioritizes security over compatibility. 267 * 268 * @return A SecurityConfiguration with strict security policies 269 */ 270 public static SecurityConfiguration strict() { 271 return builder() 272 // Path Security - very restrictive 273 .maxPathLength(1024) 274 .allowPathTraversal(false) 275 .allowDoubleEncoding(false) 276 277 // Parameter Security - strict limits 278 .maxParameterCount(50) 279 .maxParameterNameLength(100) 280 .maxParameterValueLength(1024) 281 282 // Header Security - conservative limits 283 .maxHeaderCount(50) 284 .maxHeaderNameLength(100) 285 .maxHeaderValueLength(4096) 286 287 // Cookie Security - require all security flags 288 .maxCookieCount(20) 289 .maxCookieNameLength(100) 290 .maxCookieValueLength(4096) 291 .requireSecureCookies(true) 292 .requireHttpOnlyCookies(true) 293 294 // Body Security - reasonable limit 295 .maxBodySize(10_485_760) // 10MB 296 297 // Encoding Security - no dangerous characters 298 .allowNullBytes(false) 299 .allowControlCharacters(false) 300 .allowExtendedAscii(false) 301 .normalizeUnicode(true) 302 303 // General Policies - maximum security 304 .caseSensitiveComparison(true) 305 .failOnSuspiciousPatterns(true) 306 .logSecurityViolations(true) 307 308 .build(); 309 } 310 311 /** 312 * Creates a lenient security configuration for maximum compatibility. 313 * This configuration should only be used in trusted environments. 314 * 315 * @return A SecurityConfiguration with permissive policies 316 */ 317 public static SecurityConfiguration lenient() { 318 return builder() 319 // Path Security - permissive 320 .maxPathLength(8192) 321 .allowPathTraversal(true) // WARNING: Security risk 322 .allowDoubleEncoding(true) 323 324 // Parameter Security - generous limits 325 .maxParameterCount(1000) 326 .maxParameterNameLength(512) 327 .maxParameterValueLength(8192) 328 329 // Header Security - large limits 330 .maxHeaderCount(200) 331 .maxHeaderNameLength(512) 332 .maxHeaderValueLength(16384) 333 334 // Cookie Security - no requirements 335 .maxCookieCount(100) 336 .maxCookieNameLength(512) 337 .maxCookieValueLength(8192) 338 .requireSecureCookies(false) 339 .requireHttpOnlyCookies(false) 340 341 // Body Security - large limit 342 .maxBodySize(104_857_600) // 100MB 343 344 // Encoding Security - allow everything 345 .allowNullBytes(true) 346 .allowControlCharacters(true) 347 .allowExtendedAscii(true) 348 .normalizeUnicode(false) 349 350 // General Policies - minimal security 351 .caseSensitiveComparison(false) 352 .failOnSuspiciousPatterns(false) 353 .logSecurityViolations(false) 354 355 .build(); 356 } 357 358 /** 359 * Creates a security configuration with default balanced settings. 360 * 361 * @return A SecurityConfiguration with default security policies 362 */ 363 public static SecurityConfiguration defaults() { 364 return builder().build(); 365 } 366 367 /** 368 * Checks if the configuration allows a specific header name. 369 * 370 * @param headerName The header name to check (null returns false) 371 * @return true if the header is allowed, false if blocked or null 372 */ 373 public boolean isHeaderAllowed(@Nullable String headerName) { 374 return isAllowed(headerName, allowedHeaderNames, blockedHeaderNames, 375 allowedHeaderNamesLowercase, blockedHeaderNamesLowercase); 376 } 377 378 /** 379 * Checks if the configuration allows a specific content type. 380 * 381 * @param contentType The content type to check (null returns false) 382 * @return true if the content type is allowed, false if blocked or null 383 */ 384 public boolean isContentTypeAllowed(@Nullable String contentType) { 385 return isAllowed(contentType, allowedContentTypes, blockedContentTypes, 386 allowedContentTypesLowercase, blockedContentTypesLowercase); 387 } 388 389 /** 390 * Optimized helper method to check if a value is allowed based on allow and block lists. 391 * For case-insensitive comparison, uses pre-processed lowercase sets for O(1) lookups 392 * instead of O(n) stream operations. 393 * 394 * @param value The value to check (null returns false) 395 * @param allowedSet The set of allowed values (null means all allowed) 396 * @param blockedSet The set of blocked values 397 * @param allowedSetLowercase Pre-processed lowercase allowed set (used when case-insensitive) 398 * @param blockedSetLowercase Pre-processed lowercase blocked set (used when case-insensitive) 399 * @return true if the value is allowed, false if blocked or null 400 */ 401 private boolean isAllowed(@Nullable String value, 402 @Nullable Set<String> allowedSet, 403 Set<String> blockedSet, 404 @Nullable Set<String> allowedSetLowercase, 405 Set<String> blockedSetLowercase) { 406 if (value == null) { 407 return false; 408 } 409 410 // For case-sensitive comparison, use original sets with O(1) contains 411 if (caseSensitiveComparison) { 412 // Check blocked list first 413 if (blockedSet.contains(value)) { 414 return false; 415 } 416 // If there's an allow list, check it 417 if (allowedSet != null) { 418 return allowedSet.contains(value); 419 } 420 // No allow list means all values are allowed (except blocked ones) 421 return true; 422 } 423 424 // For case-insensitive comparison, use pre-processed lowercase sets 425 // This provides O(1) average case performance instead of O(n) 426 String valueLowercase = value.toLowerCase(); 427 428 // Check blocked list first using O(1) contains 429 if (blockedSetLowercase.contains(valueLowercase)) { 430 return false; 431 } 432 433 // If there's an allow list, check it using O(1) contains 434 if (allowedSetLowercase != null) { 435 return allowedSetLowercase.contains(valueLowercase); 436 } 437 438 // No allow list means all values are allowed (except blocked ones) 439 return true; 440 } 441 442 /** 443 * Checks if this configuration is considered "strict" based on key security settings. 444 * 445 * @return true if this configuration uses strict security policies 446 */ 447 public boolean isStrict() { 448 return !allowPathTraversal && 449 !allowDoubleEncoding && 450 !allowNullBytes && 451 !allowControlCharacters && 452 !allowExtendedAscii && 453 normalizeUnicode && 454 failOnSuspiciousPatterns; 455 } 456 457 /** 458 * Checks if this configuration is considered "lenient" based on key security settings. 459 * 460 * @return true if this configuration uses lenient security policies 461 */ 462 public boolean isLenient() { 463 return allowPathTraversal && 464 allowDoubleEncoding && 465 allowNullBytes && 466 allowControlCharacters && 467 allowExtendedAscii && 468 !normalizeUnicode && 469 !failOnSuspiciousPatterns; 470 } 471 472 // Getters for all fields 473 474 public int maxPathLength() { 475 return maxPathLength; 476 } 477 478 public boolean allowPathTraversal() { 479 return allowPathTraversal; 480 } 481 482 public boolean allowDoubleEncoding() { 483 return allowDoubleEncoding; 484 } 485 486 public int maxParameterCount() { 487 return maxParameterCount; 488 } 489 490 public int maxParameterNameLength() { 491 return maxParameterNameLength; 492 } 493 494 public int maxParameterValueLength() { 495 return maxParameterValueLength; 496 } 497 498 public int maxHeaderCount() { 499 return maxHeaderCount; 500 } 501 502 public int maxHeaderNameLength() { 503 return maxHeaderNameLength; 504 } 505 506 public int maxHeaderValueLength() { 507 return maxHeaderValueLength; 508 } 509 510 public @Nullable Set<String> allowedHeaderNames() { 511 return allowedHeaderNames; 512 } 513 514 public Set<String> blockedHeaderNames() { 515 return blockedHeaderNames; 516 } 517 518 public int maxCookieCount() { 519 return maxCookieCount; 520 } 521 522 public int maxCookieNameLength() { 523 return maxCookieNameLength; 524 } 525 526 public int maxCookieValueLength() { 527 return maxCookieValueLength; 528 } 529 530 public boolean requireSecureCookies() { 531 return requireSecureCookies; 532 } 533 534 public boolean requireHttpOnlyCookies() { 535 return requireHttpOnlyCookies; 536 } 537 538 public long maxBodySize() { 539 return maxBodySize; 540 } 541 542 public @Nullable Set<String> allowedContentTypes() { 543 return allowedContentTypes; 544 } 545 546 public Set<String> blockedContentTypes() { 547 return blockedContentTypes; 548 } 549 550 public boolean allowNullBytes() { 551 return allowNullBytes; 552 } 553 554 public boolean allowControlCharacters() { 555 return allowControlCharacters; 556 } 557 558 public boolean allowExtendedAscii() { 559 return allowExtendedAscii; 560 } 561 562 public boolean normalizeUnicode() { 563 return normalizeUnicode; 564 } 565 566 public boolean caseSensitiveComparison() { 567 return caseSensitiveComparison; 568 } 569 570 public boolean failOnSuspiciousPatterns() { 571 return failOnSuspiciousPatterns; 572 } 573 574 public boolean logSecurityViolations() { 575 return logSecurityViolations; 576 } 577 578 @Override 579 public boolean equals(Object obj) { 580 if (this == obj) return true; 581 if (!(obj instanceof SecurityConfiguration other)) return false; 582 583 return maxPathLength == other.maxPathLength && 584 allowPathTraversal == other.allowPathTraversal && 585 allowDoubleEncoding == other.allowDoubleEncoding && 586 maxParameterCount == other.maxParameterCount && 587 maxParameterNameLength == other.maxParameterNameLength && 588 maxParameterValueLength == other.maxParameterValueLength && 589 maxHeaderCount == other.maxHeaderCount && 590 maxHeaderNameLength == other.maxHeaderNameLength && 591 maxHeaderValueLength == other.maxHeaderValueLength && 592 maxCookieCount == other.maxCookieCount && 593 maxCookieNameLength == other.maxCookieNameLength && 594 maxCookieValueLength == other.maxCookieValueLength && 595 requireSecureCookies == other.requireSecureCookies && 596 requireHttpOnlyCookies == other.requireHttpOnlyCookies && 597 maxBodySize == other.maxBodySize && 598 allowNullBytes == other.allowNullBytes && 599 allowControlCharacters == other.allowControlCharacters && 600 allowExtendedAscii == other.allowExtendedAscii && 601 normalizeUnicode == other.normalizeUnicode && 602 caseSensitiveComparison == other.caseSensitiveComparison && 603 failOnSuspiciousPatterns == other.failOnSuspiciousPatterns && 604 logSecurityViolations == other.logSecurityViolations && 605 Objects.equals(allowedHeaderNames, other.allowedHeaderNames) && 606 Objects.equals(blockedHeaderNames, other.blockedHeaderNames) && 607 Objects.equals(allowedContentTypes, other.allowedContentTypes) && 608 Objects.equals(blockedContentTypes, other.blockedContentTypes); 609 } 610 611 @Override 612 public int hashCode() { 613 return Objects.hash( 614 maxPathLength, allowPathTraversal, allowDoubleEncoding, 615 maxParameterCount, maxParameterNameLength, maxParameterValueLength, 616 maxHeaderCount, maxHeaderNameLength, maxHeaderValueLength, 617 allowedHeaderNames, blockedHeaderNames, 618 maxCookieCount, maxCookieNameLength, maxCookieValueLength, 619 requireSecureCookies, requireHttpOnlyCookies, 620 maxBodySize, allowedContentTypes, blockedContentTypes, 621 allowNullBytes, allowControlCharacters, allowExtendedAscii, normalizeUnicode, 622 caseSensitiveComparison, failOnSuspiciousPatterns, logSecurityViolations 623 ); 624 } 625 626 @Override 627 public String toString() { 628 return "SecurityConfiguration{" + 629 "maxPathLength=" + maxPathLength + 630 ", allowPathTraversal=" + allowPathTraversal + 631 ", allowDoubleEncoding=" + allowDoubleEncoding + 632 ", maxParameterCount=" + maxParameterCount + 633 ", maxParameterNameLength=" + maxParameterNameLength + 634 ", maxParameterValueLength=" + maxParameterValueLength + 635 ", maxHeaderCount=" + maxHeaderCount + 636 ", maxHeaderNameLength=" + maxHeaderNameLength + 637 ", maxHeaderValueLength=" + maxHeaderValueLength + 638 ", allowedHeaderNames=" + allowedHeaderNames + 639 ", blockedHeaderNames=" + blockedHeaderNames + 640 ", maxCookieCount=" + maxCookieCount + 641 ", maxCookieNameLength=" + maxCookieNameLength + 642 ", maxCookieValueLength=" + maxCookieValueLength + 643 ", requireSecureCookies=" + requireSecureCookies + 644 ", requireHttpOnlyCookies=" + requireHttpOnlyCookies + 645 ", maxBodySize=" + maxBodySize + 646 ", allowedContentTypes=" + allowedContentTypes + 647 ", blockedContentTypes=" + blockedContentTypes + 648 ", allowNullBytes=" + allowNullBytes + 649 ", allowControlCharacters=" + allowControlCharacters + 650 ", allowExtendedAscii=" + allowExtendedAscii + 651 ", normalizeUnicode=" + normalizeUnicode + 652 ", caseSensitiveComparison=" + caseSensitiveComparison + 653 ", failOnSuspiciousPatterns=" + failOnSuspiciousPatterns + 654 ", logSecurityViolations=" + logSecurityViolations + 655 '}'; 656 } 657}