001/* 002 * Copyright 2024-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.servlet.javax; 018 019import com.soklet.QueryFormat; 020import com.soklet.Request; 021import com.soklet.Utilities; 022import com.soklet.Utilities.EffectiveOriginResolver; 023import com.soklet.Utilities.EffectiveOriginResolver.TrustPolicy; 024import org.jspecify.annotations.NonNull; 025import org.jspecify.annotations.Nullable; 026 027import javax.annotation.concurrent.NotThreadSafe; 028import javax.servlet.AsyncContext; 029import javax.servlet.DispatcherType; 030import javax.servlet.RequestDispatcher; 031import javax.servlet.ServletContext; 032import javax.servlet.ServletException; 033import javax.servlet.ServletInputStream; 034import javax.servlet.ServletRequest; 035import javax.servlet.ServletResponse; 036import javax.servlet.http.Cookie; 037import javax.servlet.http.HttpServletRequest; 038import javax.servlet.http.HttpServletResponse; 039import javax.servlet.http.HttpSession; 040import javax.servlet.http.HttpUpgradeHandler; 041import javax.servlet.http.Part; 042import java.io.BufferedReader; 043import java.io.ByteArrayInputStream; 044import java.io.IOException; 045import java.io.InputStream; 046import java.io.InputStreamReader; 047import java.io.UnsupportedEncodingException; 048import java.net.InetAddress; 049import java.net.InetSocketAddress; 050import java.net.URI; 051import java.nio.charset.Charset; 052import java.nio.charset.IllegalCharsetNameException; 053import java.nio.charset.StandardCharsets; 054import java.nio.charset.UnsupportedCharsetException; 055import java.security.Principal; 056import java.time.Instant; 057import java.time.ZoneOffset; 058import java.time.format.DateTimeFormatter; 059import java.time.format.DateTimeFormatterBuilder; 060import java.time.format.SignStyle; 061import java.time.temporal.ChronoField; 062import java.util.ArrayList; 063import java.util.Collection; 064import java.util.Collections; 065import java.util.Enumeration; 066import java.util.HashMap; 067import java.util.LinkedHashMap; 068import java.util.LinkedHashSet; 069import java.util.List; 070import java.util.Locale; 071import java.util.Map; 072import java.util.Map.Entry; 073import java.util.Optional; 074import java.util.Set; 075import java.util.UUID; 076import java.util.function.Predicate; 077 078import static java.lang.String.format; 079import static java.util.Locale.ROOT; 080import static java.util.Locale.US; 081import static java.util.Locale.getDefault; 082import static java.util.Objects.requireNonNull; 083 084/** 085 * Soklet integration implementation of {@link HttpServletRequest}. 086 * 087 * @author <a href="https://www.revetkn.com">Mark Allen</a> 088 */ 089@NotThreadSafe 090public final class SokletHttpServletRequest implements HttpServletRequest { 091 @NonNull 092 private static final Charset DEFAULT_CHARSET; 093 @NonNull 094 private static final DateTimeFormatter RFC_1123_PARSER; 095 @NonNull 096 private static final DateTimeFormatter RFC_1036_PARSER; 097 @NonNull 098 private static final DateTimeFormatter ASCTIME_PARSER; 099 @NonNull 100 private static final String SESSION_COOKIE_NAME; 101 @NonNull 102 private static final String SESSION_URL_PARAM; 103 104 static { 105 DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; // Per Servlet spec 106 RFC_1123_PARSER = DateTimeFormatter.RFC_1123_DATE_TIME; 107 // RFC 1036: spaces between day/month/year + 2-digit year reduced to 19xx baseline. 108 RFC_1036_PARSER = new DateTimeFormatterBuilder() 109 .parseCaseInsensitive() 110 .appendPattern("EEE, dd MMM ") 111 .appendValueReduced(ChronoField.YEAR, 2, 2, 1900) // 94 -> 1994 112 .appendPattern(" HH:mm:ss zzz") 113 .toFormatter(US) 114 .withZone(ZoneOffset.UTC); 115 116 // asctime: "EEE MMM d HH:mm:ss yyyy" — allow 1 or 2 spaces before day, no zone in text → default GMT. 117 ASCTIME_PARSER = new DateTimeFormatterBuilder() 118 .parseCaseInsensitive() 119 .appendPattern("EEE MMM") 120 .appendLiteral(' ') 121 .optionalStart().appendLiteral(' ').optionalEnd() // tolerate double space before single-digit day 122 .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE) 123 .appendPattern(" HH:mm:ss yyyy") 124 .toFormatter(US) 125 .withZone(ZoneOffset.UTC); 126 127 SESSION_COOKIE_NAME = "JSESSIONID"; 128 SESSION_URL_PARAM = "jsessionid"; 129 } 130 131 @NonNull 132 private final Request request; 133 @Nullable 134 private final String host; 135 @Nullable 136 private final Integer port; 137 @NonNull 138 private final ServletContext servletContext; 139 @Nullable 140 private HttpSession httpSession; 141 @NonNull 142 private final Map<@NonNull String, @NonNull Object> attributes; 143 @NonNull 144 private final List<@NonNull Cookie> cookies; 145 @Nullable 146 private Charset charset; 147 @Nullable 148 private String contentType; 149 @Nullable 150 private Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters; 151 @Nullable 152 private Map<@NonNull String, @NonNull Set<@NonNull String>> formParameters; 153 private boolean parametersAccessed; 154 private boolean bodyParametersAccessed; 155 private boolean sessionCreated; 156 @NonNull 157 private final TrustPolicy forwardedHeaderTrustPolicy; 158 @Nullable 159 private final Predicate<@NonNull InetSocketAddress> trustedProxyPredicate; 160 @Nullable 161 private final Boolean allowOriginFallback; 162 @Nullable 163 private SokletServletInputStream servletInputStream; 164 @Nullable 165 private BufferedReader reader; 166 @NonNull 167 private RequestReadMethod requestReadMethod; 168 169 @NonNull 170 public static SokletHttpServletRequest fromRequest(@NonNull Request request) { 171 requireNonNull(request); 172 return withRequest(request).build(); 173 } 174 175 @NonNull 176 public static Builder withRequest(@NonNull Request request) { 177 return new Builder(request); 178 } 179 180 private SokletHttpServletRequest(@NonNull Builder builder) { 181 requireNonNull(builder); 182 requireNonNull(builder.request); 183 184 this.request = builder.request; 185 this.attributes = new HashMap<>(); 186 this.cookies = parseCookies(request); 187 this.charset = parseCharacterEncoding(request).orElse(null); 188 this.contentType = parseContentType(request).orElse(null); 189 this.host = builder.host; 190 this.port = builder.port; 191 this.servletContext = builder.servletContext == null ? SokletServletContext.fromDefaults() : builder.servletContext; 192 this.httpSession = builder.httpSession; 193 this.forwardedHeaderTrustPolicy = builder.forwardedHeaderTrustPolicy; 194 this.trustedProxyPredicate = builder.trustedProxyPredicate; 195 this.allowOriginFallback = builder.allowOriginFallback; 196 this.requestReadMethod = RequestReadMethod.UNSPECIFIED; 197 } 198 199 @NonNull 200 private Request getRequest() { 201 return this.request; 202 } 203 204 @NonNull 205 private Map<@NonNull String, @NonNull Object> getAttributes() { 206 return this.attributes; 207 } 208 209 @NonNull 210 private List<@NonNull Cookie> parseCookies(@NonNull Request request) { 211 requireNonNull(request); 212 213 List<@NonNull Cookie> convertedCookies = new ArrayList<>(); 214 Map<@NonNull String, @NonNull Set<@NonNull String>> headers = request.getHeaders(); 215 216 for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : headers.entrySet()) { 217 String headerName = entry.getKey(); 218 219 if (headerName == null || !"cookie".equalsIgnoreCase(headerName.trim())) 220 continue; 221 222 Set<@NonNull String> headerValues = entry.getValue(); 223 224 if (headerValues == null) 225 continue; 226 227 for (String headerValue : headerValues) { 228 headerValue = Utilities.trimAggressivelyToNull(headerValue); 229 230 if (headerValue == null) 231 continue; 232 233 for (String cookieComponent : splitCookieHeaderRespectingQuotes(headerValue)) { 234 cookieComponent = Utilities.trimAggressivelyToNull(cookieComponent); 235 236 if (cookieComponent == null) 237 continue; 238 239 String[] cookiePair = cookieComponent.split("=", 2); 240 String rawName = Utilities.trimAggressivelyToNull(cookiePair[0]); 241 if (cookiePair.length != 2) 242 continue; 243 244 String rawValue = Utilities.trimAggressivelyToEmpty(cookiePair[1]); 245 246 if (rawName == null) 247 continue; 248 249 String cookieValue = unquoteCookieValueIfNeeded(rawValue); 250 convertedCookies.add(new Cookie(rawName, cookieValue)); 251 } 252 } 253 } 254 255 return convertedCookies; 256 } 257 258 /** 259 * Splits a Cookie header string into components on ';' but ONLY when not inside a quoted value. 260 * Supports backslash-escaped quotes within quoted strings. 261 */ 262 @NonNull 263 private static List<@NonNull String> splitCookieHeaderRespectingQuotes(@NonNull String headerValue) { 264 List<@NonNull String> parts = new ArrayList<>(); 265 StringBuilder current = new StringBuilder(headerValue.length()); 266 boolean inQuotes = false; 267 boolean escape = false; 268 269 for (int i = 0; i < headerValue.length(); i++) { 270 char c = headerValue.charAt(i); 271 272 if (escape) { 273 current.append(c); 274 escape = false; 275 continue; 276 } 277 278 if (c == '\\') { 279 escape = true; 280 current.append(c); 281 continue; 282 } 283 284 if (c == '"') { 285 inQuotes = !inQuotes; 286 current.append(c); 287 continue; 288 } 289 290 if (c == ';' && !inQuotes) { 291 parts.add(current.toString()); 292 current.setLength(0); 293 continue; 294 } 295 296 current.append(c); 297 } 298 299 if (current.length() > 0) 300 parts.add(current.toString()); 301 302 return parts; 303 } 304 305 /** 306 * Splits a header value on the given delimiter, ignoring delimiters inside quoted strings. 307 * Supports backslash-escaped quotes within quoted strings. 308 */ 309 @NonNull 310 private static List<@NonNull String> splitHeaderValueRespectingQuotes(@NonNull String headerValue, 311 char delimiter) { 312 List<@NonNull String> parts = new ArrayList<>(); 313 StringBuilder current = new StringBuilder(headerValue.length()); 314 boolean inQuotes = false; 315 boolean escape = false; 316 317 for (int i = 0; i < headerValue.length(); i++) { 318 char c = headerValue.charAt(i); 319 320 if (escape) { 321 current.append(c); 322 escape = false; 323 continue; 324 } 325 326 if (c == '\\') { 327 escape = true; 328 current.append(c); 329 continue; 330 } 331 332 if (c == '"') { 333 inQuotes = !inQuotes; 334 current.append(c); 335 continue; 336 } 337 338 if (c == delimiter && !inQuotes) { 339 parts.add(current.toString()); 340 current.setLength(0); 341 continue; 342 } 343 344 current.append(c); 345 } 346 347 if (current.length() > 0) 348 parts.add(current.toString()); 349 350 return parts; 351 } 352 353 /** 354 * If the cookie value is a quoted-string, remove surrounding quotes and unescape \" \\ and \; . 355 * Otherwise returns the input as-is. 356 */ 357 @NonNull 358 private static String unquoteCookieValueIfNeeded(@NonNull String rawValue) { 359 requireNonNull(rawValue); 360 361 if (rawValue.length() >= 2 && rawValue.charAt(0) == '"' && rawValue.charAt(rawValue.length() - 1) == '"') { 362 String inner = rawValue.substring(1, rawValue.length() - 1); 363 StringBuilder sb = new StringBuilder(inner.length()); 364 boolean escape = false; 365 366 for (int i = 0; i < inner.length(); i++) { 367 char c = inner.charAt(i); 368 369 if (escape) { 370 sb.append(c); 371 escape = false; 372 } else if (c == '\\') { 373 escape = true; 374 } else { 375 sb.append(c); 376 } 377 } 378 379 if (escape) 380 sb.append('\\'); 381 382 return sb.toString(); 383 } 384 385 return rawValue; 386 } 387 388 /** 389 * Remove a single pair of surrounding quotes if present. 390 */ 391 @NonNull 392 private static String stripOptionalQuotes(@NonNull String value) { 393 requireNonNull(value); 394 395 if (value.length() >= 2) { 396 char first = value.charAt(0); 397 char last = value.charAt(value.length() - 1); 398 399 if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) 400 return value.substring(1, value.length() - 1); 401 } 402 403 return value; 404 } 405 406 @NonNull 407 private Optional<Charset> parseCharacterEncoding(@NonNull Request request) { 408 requireNonNull(request); 409 return Utilities.extractCharsetFromHeaders(request.getHeaders()); 410 } 411 412 @NonNull 413 private Optional<String> parseContentType(@NonNull Request request) { 414 requireNonNull(request); 415 return Utilities.extractContentTypeFromHeaders(request.getHeaders()); 416 } 417 418 @NonNull 419 private Optional<HttpSession> getHttpSession() { 420 HttpSession current = this.httpSession; 421 422 if (current instanceof SokletHttpSession && ((SokletHttpSession) current).isInvalidated()) { 423 this.httpSession = null; 424 return Optional.empty(); 425 } 426 427 return Optional.ofNullable(current); 428 } 429 430 private void setHttpSession(@Nullable HttpSession httpSession) { 431 this.httpSession = httpSession; 432 } 433 434 private void touchSession(@NonNull HttpSession httpSession, 435 boolean createdNow) { 436 requireNonNull(httpSession); 437 438 if (httpSession instanceof SokletHttpSession) { 439 SokletHttpSession sokletSession = (SokletHttpSession) httpSession; 440 sokletSession.markAccessed(); 441 442 if (!createdNow && !this.sessionCreated) 443 sokletSession.markNotNew(); 444 } 445 } 446 447 @NonNull 448 private Optional<Charset> getCharset() { 449 return Optional.ofNullable(this.charset); 450 } 451 452 @Nullable 453 private Charset getContextRequestCharset() { 454 String encoding = getServletContext().getRequestCharacterEncoding(); 455 456 if (encoding == null || encoding.isBlank()) 457 return null; 458 459 try { 460 return Charset.forName(encoding); 461 } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 462 return null; 463 } 464 } 465 466 @NonNull 467 private Charset getEffectiveCharset() { 468 Charset explicit = this.charset; 469 470 if (explicit != null) 471 return explicit; 472 473 Charset context = getContextRequestCharset(); 474 return context == null ? DEFAULT_CHARSET : context; 475 } 476 477 @Nullable 478 private Long getContentLengthHeaderValue() { 479 String value = getHeader("Content-Length"); 480 481 if (value == null) 482 return null; 483 484 value = value.trim(); 485 486 if (value.isEmpty()) 487 return null; 488 489 try { 490 long parsed = Long.parseLong(value, 10); 491 return parsed < 0 ? null : parsed; 492 } catch (NumberFormatException e) { 493 return null; 494 } 495 } 496 497 private boolean hasContentLengthHeader() { 498 Set<@NonNull String> values = getRequest().getHeaders().get("Content-Length"); 499 return values != null && !values.isEmpty(); 500 } 501 502 private void setCharset(@Nullable Charset charset) { 503 this.charset = charset; 504 } 505 506 @NonNull 507 private Map<@NonNull String, @NonNull Set<@NonNull String>> getQueryParameters() { 508 if (this.queryParameters != null) 509 return this.queryParameters; 510 511 String rawQuery = getRequest().getRawQuery().orElse(null); 512 513 if (rawQuery == null || rawQuery.isEmpty()) { 514 this.queryParameters = Map.of(); 515 return this.queryParameters; 516 } 517 518 Charset charset = getEffectiveCharset(); 519 Map<@NonNull String, @NonNull Set<@NonNull String>> parsed = 520 Utilities.extractQueryParametersFromQuery(rawQuery, QueryFormat.X_WWW_FORM_URLENCODED, charset); 521 this.queryParameters = Collections.unmodifiableMap(parsed); 522 return this.queryParameters; 523 } 524 525 @NonNull 526 private Map<@NonNull String, @NonNull Set<@NonNull String>> getFormParameters() { 527 if (this.formParameters != null) 528 return this.formParameters; 529 530 if (getRequestReadMethod() != RequestReadMethod.UNSPECIFIED) { 531 this.formParameters = Map.of(); 532 return this.formParameters; 533 } 534 535 if (this.contentType == null || !this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) { 536 this.formParameters = Map.of(); 537 return this.formParameters; 538 } 539 540 markBodyParametersAccessed(); 541 542 byte[] body = getRequest().getBody().orElse(null); 543 544 if (body == null || body.length == 0) { 545 this.formParameters = Map.of(); 546 return this.formParameters; 547 } 548 549 String bodyAsString = new String(body, StandardCharsets.ISO_8859_1); 550 Charset charset = getEffectiveCharset(); 551 Map<@NonNull String, @NonNull Set<@NonNull String>> parsed = 552 Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, charset); 553 this.formParameters = Collections.unmodifiableMap(parsed); 554 return this.formParameters; 555 } 556 557 private void markParametersAccessed() { 558 this.parametersAccessed = true; 559 } 560 561 private void markBodyParametersAccessed() { 562 this.bodyParametersAccessed = true; 563 } 564 565 private boolean shouldTrustForwardedHeaders() { 566 if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_ALL) 567 return true; 568 569 if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_NONE) 570 return false; 571 572 if (this.trustedProxyPredicate == null) 573 return false; 574 575 InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null); 576 return remoteAddress != null && this.trustedProxyPredicate.test(remoteAddress); 577 } 578 579 @Nullable 580 private ForwardedClient extractForwardedClientFromHeaders() { 581 Set<@NonNull String> headerValues = getRequest().getHeaders().get("Forwarded"); 582 583 if (headerValues == null) 584 return null; 585 586 for (String headerValue : headerValues) { 587 ForwardedClient candidate = extractForwardedClientFromHeaderValue(headerValue); 588 589 if (candidate != null) 590 return candidate; 591 } 592 593 return null; 594 } 595 596 @Nullable 597 private ForwardedClient extractForwardedClientFromHeaderValue(@Nullable String headerValue) { 598 headerValue = Utilities.trimAggressivelyToNull(headerValue); 599 600 if (headerValue == null) 601 return null; 602 603 for (String forwardedEntry : splitHeaderValueRespectingQuotes(headerValue, ',')) { 604 forwardedEntry = Utilities.trimAggressivelyToNull(forwardedEntry); 605 606 if (forwardedEntry == null) 607 continue; 608 609 for (String component : splitHeaderValueRespectingQuotes(forwardedEntry, ';')) { 610 component = Utilities.trimAggressivelyToNull(component); 611 612 if (component == null) 613 continue; 614 615 String[] nameValue = component.split("=", 2); 616 617 if (nameValue.length != 2) 618 continue; 619 620 String name = Utilities.trimAggressivelyToNull(nameValue[0]); 621 622 if (name == null || !"for".equalsIgnoreCase(name)) 623 continue; 624 625 String value = Utilities.trimAggressivelyToNull(nameValue[1]); 626 627 if (value == null) 628 continue; 629 630 value = stripOptionalQuotes(value); 631 value = Utilities.trimAggressivelyToNull(value); 632 633 if (value == null) 634 continue; 635 636 ForwardedClient normalized = parseForwardedForValue(value); 637 638 if (normalized != null) 639 return normalized; 640 } 641 } 642 643 return null; 644 } 645 646 @Nullable 647 private ForwardedClient parseForwardedForValue(@NonNull String value) { 648 requireNonNull(value); 649 650 String normalized = value.trim(); 651 652 if (normalized.isEmpty()) 653 return null; 654 655 if ("unknown".equalsIgnoreCase(normalized) || normalized.startsWith("_")) 656 return null; 657 658 if (normalized.startsWith("[")) { 659 int close = normalized.indexOf(']'); 660 661 if (close > 0) { 662 String host = normalized.substring(1, close); 663 664 if (host.isEmpty()) 665 return null; 666 667 Integer port = null; 668 String rest = normalized.substring(close + 1).trim(); 669 670 if (!rest.isEmpty()) { 671 if (!rest.startsWith(":")) 672 return null; 673 674 String portToken = Utilities.trimAggressivelyToNull(rest.substring(1)); 675 676 if (portToken != null) { 677 try { 678 port = Integer.parseInt(portToken, 10); 679 } catch (Exception ignored) { 680 // Ignore invalid port. 681 } 682 } 683 } 684 685 return new ForwardedClient(host, port); 686 } 687 688 return null; 689 } 690 691 int colonCount = 0; 692 693 for (int i = 0; i < normalized.length(); i++) { 694 if (normalized.charAt(i) == ':') 695 colonCount++; 696 } 697 698 if (colonCount == 0) 699 return new ForwardedClient(normalized, null); 700 701 if (colonCount == 1) { 702 int colon = normalized.indexOf(':'); 703 String host = normalized.substring(0, colon).trim(); 704 705 if (host.isEmpty()) 706 return null; 707 708 String portToken = Utilities.trimAggressivelyToNull(normalized.substring(colon + 1)); 709 Integer port = null; 710 711 if (portToken != null) { 712 try { 713 port = Integer.parseInt(portToken, 10); 714 } catch (Exception ignored) { 715 // Ignore invalid port. 716 } 717 } 718 719 return new ForwardedClient(host, port); 720 } 721 722 return new ForwardedClient(normalized, null); 723 } 724 725 @Nullable 726 private ForwardedClient extractXForwardedClientFromHeaders() { 727 Set<@NonNull String> headerValues = getRequest().getHeaders().get("X-Forwarded-For"); 728 729 if (headerValues == null) 730 return null; 731 732 for (String headerValue : headerValues) { 733 if (headerValue == null) 734 continue; 735 736 String[] components = headerValue.split(","); 737 738 for (String component : components) { 739 String value = Utilities.trimAggressivelyToNull(component); 740 741 if (value != null) { 742 value = stripOptionalQuotes(value); 743 value = Utilities.trimAggressivelyToNull(value); 744 745 if (value != null) { 746 ForwardedClient normalized = parseForwardedForValue(value); 747 748 if (normalized != null) 749 return normalized; 750 } 751 } 752 } 753 } 754 755 return null; 756 } 757 758 private static final class ForwardedClient { 759 @NonNull 760 private final String host; 761 @Nullable 762 private final Integer port; 763 764 private ForwardedClient(@NonNull String host, 765 @Nullable Integer port) { 766 this.host = requireNonNull(host); 767 this.port = port; 768 } 769 770 @NonNull 771 private String getHost() { 772 return this.host; 773 } 774 775 @Nullable 776 private Integer getPort() { 777 return this.port; 778 } 779 } 780 781 @NonNull 782 private Optional<String> getHost() { 783 return Optional.ofNullable(this.host); 784 } 785 786 @NonNull 787 private Optional<Integer> getPort() { 788 return Optional.ofNullable(this.port); 789 } 790 791 @NonNull 792 private Optional<String> getEffectiveOrigin() { 793 EffectiveOriginResolver resolver = EffectiveOriginResolver.withRequest( 794 getRequest(), 795 this.forwardedHeaderTrustPolicy 796 ); 797 798 if (this.trustedProxyPredicate != null) 799 resolver.trustedProxyPredicate(this.trustedProxyPredicate); 800 801 if (this.allowOriginFallback != null) 802 resolver.allowOriginFallback(this.allowOriginFallback); 803 804 return Utilities.extractEffectiveOrigin(resolver); 805 } 806 807 @NonNull 808 private Optional<URI> getEffectiveOriginUri() { 809 String effectiveOrigin = getEffectiveOrigin().orElse(null); 810 811 if (effectiveOrigin == null) 812 return Optional.empty(); 813 814 try { 815 return Optional.of(URI.create(effectiveOrigin)); 816 } catch (Exception ignored) { 817 return Optional.empty(); 818 } 819 } 820 821 private int defaultPortForScheme(@Nullable String scheme) { 822 if (scheme == null) 823 return 0; 824 825 if ("https".equalsIgnoreCase(scheme)) 826 return 443; 827 828 if ("http".equalsIgnoreCase(scheme)) 829 return 80; 830 831 return 0; 832 } 833 834 @NonNull 835 private String stripIpv6Brackets(@NonNull String host) { 836 requireNonNull(host); 837 838 if (host.startsWith("[") && host.endsWith("]") && host.length() > 2) 839 return host.substring(1, host.length() - 1); 840 841 return host; 842 } 843 844 private boolean isIpv4Literal(@NonNull String value) { 845 requireNonNull(value); 846 String[] parts = value.split("\\.", -1); 847 848 if (parts.length != 4) 849 return false; 850 851 for (String part : parts) { 852 if (part.isEmpty()) 853 return false; 854 855 int acc = 0; 856 857 for (int i = 0; i < part.length(); i++) { 858 char c = part.charAt(i); 859 if (c < '0' || c > '9') 860 return false; 861 acc = acc * 10 + (c - '0'); 862 if (acc > 255) 863 return false; 864 } 865 } 866 867 return true; 868 } 869 870 private boolean isIpv6Literal(@NonNull String value) { 871 requireNonNull(value); 872 return value.indexOf(':') >= 0; 873 } 874 875 @Nullable 876 private String hostFromAuthority(@Nullable String authority) { 877 if (authority == null) 878 return null; 879 880 String normalized = authority.trim(); 881 882 if (normalized.isEmpty()) 883 return null; 884 885 int at = normalized.lastIndexOf('@'); 886 887 if (at >= 0) 888 normalized = normalized.substring(at + 1); 889 890 if (normalized.startsWith("[")) { 891 int close = normalized.indexOf(']'); 892 893 if (close > 0) 894 return normalized.substring(1, close); 895 896 return null; 897 } 898 899 int colon = normalized.indexOf(':'); 900 return colon > 0 ? normalized.substring(0, colon) : normalized; 901 } 902 903 @Nullable 904 private Integer portFromAuthority(@Nullable String authority) { 905 if (authority == null) 906 return null; 907 908 String normalized = authority.trim(); 909 910 if (normalized.isEmpty()) 911 return null; 912 913 int at = normalized.lastIndexOf('@'); 914 915 if (at >= 0) 916 normalized = normalized.substring(at + 1); 917 918 if (normalized.startsWith("[")) { 919 int close = normalized.indexOf(']'); 920 921 if (close > 0 && normalized.length() > close + 1 && normalized.charAt(close + 1) == ':') { 922 String portString = normalized.substring(close + 2).trim(); 923 924 try { 925 return Integer.parseInt(portString, 10); 926 } catch (Exception ignored) { 927 return null; 928 } 929 } 930 931 return null; 932 } 933 934 int colon = normalized.indexOf(':'); 935 936 if (colon > 0 && normalized.indexOf(':', colon + 1) == -1) { 937 String portString = normalized.substring(colon + 1).trim(); 938 939 try { 940 return Integer.parseInt(portString, 10); 941 } catch (Exception ignored) { 942 return null; 943 } 944 } 945 946 return null; 947 } 948 949 @NonNull 950 private Optional<SokletServletInputStream> getServletInputStream() { 951 return Optional.ofNullable(this.servletInputStream); 952 } 953 954 private void setServletInputStream(@Nullable SokletServletInputStream servletInputStream) { 955 this.servletInputStream = servletInputStream; 956 } 957 958 @NonNull 959 private Optional<BufferedReader> getBufferedReader() { 960 return Optional.ofNullable(this.reader); 961 } 962 963 private void setBufferedReader(@Nullable BufferedReader reader) { 964 this.reader = reader; 965 } 966 967 @NonNull 968 private RequestReadMethod getRequestReadMethod() { 969 return this.requestReadMethod; 970 } 971 972 private void setRequestReadMethod(@NonNull RequestReadMethod requestReadMethod) { 973 requireNonNull(requestReadMethod); 974 this.requestReadMethod = requestReadMethod; 975 } 976 977 private enum RequestReadMethod { 978 UNSPECIFIED, 979 INPUT_STREAM, 980 READER 981 } 982 983 /** 984 * Builder used to construct instances of {@link SokletHttpServletRequest}. 985 * <p> 986 * This class is intended for use by a single thread. 987 * 988 * @author <a href="https://www.revetkn.com">Mark Allen</a> 989 */ 990 @NotThreadSafe 991 public static class Builder { 992 @NonNull 993 private Request request; 994 @Nullable 995 private Integer port; 996 @Nullable 997 private String host; 998 @Nullable 999 private ServletContext servletContext; 1000 @Nullable 1001 private HttpSession httpSession; 1002 @NonNull 1003 private TrustPolicy forwardedHeaderTrustPolicy; 1004 @Nullable 1005 private Predicate<@NonNull InetSocketAddress> trustedProxyPredicate; 1006 @Nullable 1007 private Boolean allowOriginFallback; 1008 1009 @NonNull 1010 private Builder(@NonNull Request request) { 1011 requireNonNull(request); 1012 this.request = request; 1013 this.forwardedHeaderTrustPolicy = TrustPolicy.TRUST_NONE; 1014 } 1015 1016 @NonNull 1017 public Builder request(@NonNull Request request) { 1018 requireNonNull(request); 1019 this.request = request; 1020 return this; 1021 } 1022 1023 @NonNull 1024 public Builder host(@Nullable String host) { 1025 this.host = host; 1026 return this; 1027 } 1028 1029 @NonNull 1030 public Builder port(@Nullable Integer port) { 1031 this.port = port; 1032 return this; 1033 } 1034 1035 @NonNull 1036 public Builder servletContext(@Nullable ServletContext servletContext) { 1037 this.servletContext = servletContext; 1038 return this; 1039 } 1040 1041 @NonNull 1042 public Builder httpSession(@Nullable HttpSession httpSession) { 1043 this.httpSession = httpSession; 1044 return this; 1045 } 1046 1047 @NonNull 1048 public Builder forwardedHeaderTrustPolicy(@NonNull TrustPolicy forwardedHeaderTrustPolicy) { 1049 requireNonNull(forwardedHeaderTrustPolicy); 1050 this.forwardedHeaderTrustPolicy = forwardedHeaderTrustPolicy; 1051 return this; 1052 } 1053 1054 @NonNull 1055 public Builder trustedProxyPredicate(@Nullable Predicate<@NonNull InetSocketAddress> trustedProxyPredicate) { 1056 this.trustedProxyPredicate = trustedProxyPredicate; 1057 return this; 1058 } 1059 1060 @NonNull 1061 public Builder trustedProxyAddresses(@NonNull Set<@NonNull InetAddress> trustedProxyAddresses) { 1062 requireNonNull(trustedProxyAddresses); 1063 Set<@NonNull InetAddress> normalizedAddresses = Set.copyOf(trustedProxyAddresses); 1064 this.trustedProxyPredicate = remoteAddress -> { 1065 if (remoteAddress == null) 1066 return false; 1067 1068 InetAddress address = remoteAddress.getAddress(); 1069 return address != null && normalizedAddresses.contains(address); 1070 }; 1071 return this; 1072 } 1073 1074 @NonNull 1075 public Builder allowOriginFallback(@Nullable Boolean allowOriginFallback) { 1076 this.allowOriginFallback = allowOriginFallback; 1077 return this; 1078 } 1079 1080 @NonNull 1081 public SokletHttpServletRequest build() { 1082 if (this.forwardedHeaderTrustPolicy == TrustPolicy.TRUST_PROXY_ALLOWLIST 1083 && this.trustedProxyPredicate == null) { 1084 throw new IllegalStateException(format("%s policy requires a trusted proxy predicate or allowlist.", 1085 TrustPolicy.TRUST_PROXY_ALLOWLIST)); 1086 } 1087 1088 return new SokletHttpServletRequest(this); 1089 } 1090 } 1091 1092 // Implementation of HttpServletRequest methods below: 1093 1094 // Helpful reference at https://stackoverflow.com/a/21046620 by Victor Stafusa - BozoNaCadeia 1095 // 1096 // Method URL-Decoded Result 1097 // ---------------------------------------------------- 1098 // getContextPath() no /app 1099 // getLocalAddr() 127.0.0.1 1100 // getLocalName() 30thh.loc 1101 // getLocalPort() 8480 1102 // getMethod() GET 1103 // getPathInfo() yes /a?+b 1104 // getProtocol() HTTP/1.1 1105 // getQueryString() no p+1=c+d&p+2=e+f 1106 // getRequestedSessionId() no S%3F+ID 1107 // getRequestURI() no /app/test%3F/a%3F+b;jsessionid=S+ID 1108 // getRequestURL() no http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID 1109 // getScheme() http 1110 // getServerName() 30thh.loc 1111 // getServerPort() 8480 1112 // getServletPath() yes /test? 1113 // getParameterNames() yes [p 2, p 1] 1114 // getParameter("p 1") yes c d 1115 1116 @Override 1117 @Nullable 1118 public String getAuthType() { 1119 // This is legal according to spec 1120 return null; 1121 } 1122 1123 @Override 1124 public @NonNull Cookie @Nullable [] getCookies() { 1125 return this.cookies.isEmpty() ? null : this.cookies.toArray(new Cookie[0]); 1126 } 1127 1128 @Override 1129 public long getDateHeader(@Nullable String name) { 1130 if (name == null) 1131 return -1; 1132 1133 String value = getHeader(name); 1134 1135 if (value == null) 1136 return -1; 1137 1138 // Try HTTP-date formats (RFC 1123 → RFC 1036 → asctime) 1139 for (DateTimeFormatter fmt : List.of(RFC_1123_PARSER, RFC_1036_PARSER, ASCTIME_PARSER)) { 1140 try { 1141 return Instant.from(fmt.parse(value)).toEpochMilli(); 1142 } catch (Exception ignored) { 1143 // try next 1144 } 1145 } 1146 1147 // Fallback: epoch millis 1148 try { 1149 return Long.parseLong(value); 1150 } catch (NumberFormatException e) { 1151 throw new IllegalArgumentException( 1152 String.format("Header with name '%s' and value '%s' cannot be converted to a date", name, value), 1153 e 1154 ); 1155 } 1156 } 1157 1158 @Override 1159 @Nullable 1160 public String getHeader(@Nullable String name) { 1161 if (name == null) 1162 return null; 1163 1164 Set<@NonNull String> values = getRequest().getHeaders().get(name); 1165 1166 if (values == null || values.isEmpty()) 1167 return null; 1168 1169 return values.iterator().next(); 1170 } 1171 1172 @Override 1173 @NonNull 1174 public Enumeration<@NonNull String> getHeaders(@Nullable String name) { 1175 if (name == null) 1176 return Collections.emptyEnumeration(); 1177 1178 Set<@NonNull String> values = request.getHeaders().get(name); 1179 return values == null ? Collections.emptyEnumeration() : Collections.enumeration(values); 1180 } 1181 1182 @Override 1183 @NonNull 1184 public Enumeration<@NonNull String> getHeaderNames() { 1185 return Collections.enumeration(getRequest().getHeaders().keySet()); 1186 } 1187 1188 @Override 1189 public int getIntHeader(@Nullable String name) { 1190 if (name == null) 1191 return -1; 1192 1193 String value = getHeader(name); 1194 1195 if (value == null) 1196 return -1; 1197 1198 // Throws NumberFormatException if parsing fails, per spec 1199 return Integer.valueOf(value, 10); 1200 } 1201 1202 @Override 1203 @NonNull 1204 public String getMethod() { 1205 return getRequest().getHttpMethod().name(); 1206 } 1207 1208 @Override 1209 @Nullable 1210 public String getPathInfo() { 1211 return getRequest().getPath(); 1212 } 1213 1214 @Override 1215 @Nullable 1216 public String getPathTranslated() { 1217 return null; 1218 } 1219 1220 @Override 1221 @NonNull 1222 public String getContextPath() { 1223 return ""; 1224 } 1225 1226 @Override 1227 @Nullable 1228 public String getQueryString() { 1229 return getRequest().getRawQuery().orElse(null); 1230 } 1231 1232 @Override 1233 @Nullable 1234 public String getRemoteUser() { 1235 // This is legal according to spec 1236 return null; 1237 } 1238 1239 @Override 1240 public boolean isUserInRole(@Nullable String role) { 1241 // This is legal according to spec 1242 return false; 1243 } 1244 1245 @Override 1246 @Nullable 1247 public Principal getUserPrincipal() { 1248 // This is legal according to spec 1249 return null; 1250 } 1251 1252 @Nullable 1253 private String extractRequestedSessionIdFromCookie() { 1254 for (Cookie cookie : this.cookies) { 1255 String name = cookie.getName(); 1256 1257 if (name != null && SESSION_COOKIE_NAME.equalsIgnoreCase(name)) { 1258 String value = cookie.getValue(); 1259 1260 if (value != null && !value.isEmpty()) 1261 return value; 1262 } 1263 } 1264 1265 return null; 1266 } 1267 1268 @Nullable 1269 private String extractRequestedSessionIdFromUrl() { 1270 String rawPath = getRequest().getRawPath(); 1271 int length = rawPath.length(); 1272 int index = 0; 1273 1274 while (index < length) { 1275 int semicolon = rawPath.indexOf(';', index); 1276 1277 if (semicolon < 0) 1278 break; 1279 1280 int nameStart = semicolon + 1; 1281 1282 if (nameStart >= length) 1283 break; 1284 1285 int nameEnd = nameStart; 1286 1287 while (nameEnd < length) { 1288 char ch = rawPath.charAt(nameEnd); 1289 1290 if (ch == '=' || ch == ';' || ch == '/') 1291 break; 1292 1293 nameEnd++; 1294 } 1295 1296 if (nameEnd == nameStart) { 1297 index = nameEnd + 1; 1298 continue; 1299 } 1300 1301 String name = rawPath.substring(nameStart, nameEnd); 1302 1303 if (!SESSION_URL_PARAM.equalsIgnoreCase(name)) { 1304 index = nameEnd + 1; 1305 continue; 1306 } 1307 1308 if (nameEnd >= length || rawPath.charAt(nameEnd) != '=') { 1309 index = nameEnd + 1; 1310 continue; 1311 } 1312 1313 int valueStart = nameEnd + 1; 1314 int valueEnd = valueStart; 1315 1316 while (valueEnd < length) { 1317 char ch = rawPath.charAt(valueEnd); 1318 1319 if (ch == ';' || ch == '/') 1320 break; 1321 1322 valueEnd++; 1323 } 1324 1325 if (valueEnd == valueStart) { 1326 index = valueEnd + 1; 1327 continue; 1328 } 1329 1330 String value = rawPath.substring(valueStart, valueEnd); 1331 1332 if (!value.isEmpty()) 1333 return value; 1334 1335 index = valueEnd + 1; 1336 } 1337 1338 return null; 1339 } 1340 1341 @Override 1342 @Nullable 1343 public String getRequestedSessionId() { 1344 String cookieSessionId = extractRequestedSessionIdFromCookie(); 1345 1346 if (cookieSessionId != null) 1347 return cookieSessionId; 1348 1349 return extractRequestedSessionIdFromUrl(); 1350 } 1351 1352 @Override 1353 @NonNull 1354 public String getRequestURI() { 1355 return getRequest().getRawPath(); 1356 } 1357 1358 @Override 1359 @NonNull 1360 public StringBuffer getRequestURL() { 1361 String rawPath = getRequest().getRawPath(); 1362 1363 if ("*".equals(rawPath)) 1364 return new StringBuffer(rawPath); 1365 1366 // Try forwarded/synthesized absolute prefix first 1367 String effectiveOrigin = getEffectiveOrigin().orElse(null); 1368 1369 if (effectiveOrigin != null) 1370 return new StringBuffer(format("%s%s", effectiveOrigin, rawPath)); 1371 1372 // Fall back to builder-provided host/port when available 1373 String scheme = getScheme(); // Soklet returns "http" by design 1374 String host = getServerName(); 1375 int port = getServerPort(); 1376 boolean defaultPort = port <= 0 || ("https".equalsIgnoreCase(scheme) && port == 443) || ("http".equalsIgnoreCase(scheme) && port == 80); 1377 String authorityHost = host; 1378 1379 if (host != null && host.indexOf(':') >= 0 && !host.startsWith("[") && !host.endsWith("]")) 1380 authorityHost = "[" + host + "]"; 1381 1382 String authority = defaultPort ? authorityHost : format("%s:%d", authorityHost, port); 1383 return new StringBuffer(format("%s://%s%s", scheme, authority, rawPath)); 1384 } 1385 1386 @Override 1387 @NonNull 1388 public String getServletPath() { 1389 // This is legal according to spec 1390 return ""; 1391 } 1392 1393 @Override 1394 @Nullable 1395 public HttpSession getSession(boolean create) { 1396 HttpSession currentHttpSession = getHttpSession().orElse(null); 1397 boolean createdNow = false; 1398 1399 if (create && currentHttpSession == null) { 1400 currentHttpSession = SokletHttpSession.fromServletContext(getServletContext()); 1401 setHttpSession(currentHttpSession); 1402 this.sessionCreated = true; 1403 createdNow = true; 1404 } 1405 1406 if (currentHttpSession != null) 1407 touchSession(currentHttpSession, createdNow); 1408 1409 return currentHttpSession; 1410 } 1411 1412 @Override 1413 @NonNull 1414 public HttpSession getSession() { 1415 HttpSession currentHttpSession = getHttpSession().orElse(null); 1416 boolean createdNow = false; 1417 1418 if (currentHttpSession == null) { 1419 currentHttpSession = SokletHttpSession.fromServletContext(getServletContext()); 1420 setHttpSession(currentHttpSession); 1421 this.sessionCreated = true; 1422 createdNow = true; 1423 } 1424 1425 touchSession(currentHttpSession, createdNow); 1426 1427 return currentHttpSession; 1428 } 1429 1430 @Override 1431 @NonNull 1432 public String changeSessionId() { 1433 HttpSession currentHttpSession = getHttpSession().orElse(null); 1434 1435 if (currentHttpSession == null) 1436 throw new IllegalStateException("No session is present"); 1437 1438 if (!(currentHttpSession instanceof SokletHttpSession)) 1439 throw new IllegalStateException(format("Cannot change session IDs. Session must be of type %s; instead it is of type %s", 1440 SokletHttpSession.class.getSimpleName(), currentHttpSession.getClass().getSimpleName())); 1441 1442 UUID newSessionId = UUID.randomUUID(); 1443 ((SokletHttpSession) currentHttpSession).setSessionId(newSessionId); 1444 return String.valueOf(newSessionId); 1445 } 1446 1447 @Override 1448 public boolean isRequestedSessionIdValid() { 1449 String requestedSessionId = getRequestedSessionId(); 1450 1451 if (requestedSessionId == null) 1452 return false; 1453 1454 HttpSession currentSession = getHttpSession().orElse(null); 1455 1456 if (currentSession == null) 1457 return false; 1458 1459 return requestedSessionId.equals(currentSession.getId()); 1460 } 1461 1462 @Override 1463 public boolean isRequestedSessionIdFromCookie() { 1464 return extractRequestedSessionIdFromCookie() != null; 1465 } 1466 1467 @Override 1468 public boolean isRequestedSessionIdFromURL() { 1469 if (extractRequestedSessionIdFromCookie() != null) 1470 return false; 1471 1472 return extractRequestedSessionIdFromUrl() != null; 1473 } 1474 1475 @Override 1476 @Deprecated 1477 public boolean isRequestedSessionIdFromUrl() { 1478 return isRequestedSessionIdFromURL(); 1479 } 1480 1481 @Override 1482 public boolean authenticate(@NonNull HttpServletResponse httpServletResponse) throws IOException, ServletException { 1483 requireNonNull(httpServletResponse); 1484 // TODO: perhaps revisit this in the future 1485 throw new ServletException("Authentication is not supported"); 1486 } 1487 1488 @Override 1489 public void login(@Nullable String username, 1490 @Nullable String password) throws ServletException { 1491 // This is legal according to spec 1492 throw new ServletException("Authentication login is not supported"); 1493 } 1494 1495 @Override 1496 public void logout() throws ServletException { 1497 // This is legal according to spec 1498 throw new ServletException("Authentication logout is not supported"); 1499 } 1500 1501 @Override 1502 @NonNull 1503 public Collection<@NonNull Part> getParts() throws IOException, ServletException { 1504 // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize, 1505 // or there is no @MultipartConfig or multipart-config in deployment descriptors 1506 throw new ServletException("Servlet multipart configuration is not supported"); 1507 } 1508 1509 @Override 1510 @Nullable 1511 public Part getPart(@Nullable String name) throws IOException, ServletException { 1512 // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize, 1513 // or there is no @MultipartConfig or multipart-config in deployment descriptors 1514 throw new ServletException("Servlet multipart configuration is not supported"); 1515 } 1516 1517 @Override 1518 @NonNull 1519 public <T extends HttpUpgradeHandler> T upgrade(@Nullable Class<T> handlerClass) throws IOException, ServletException { 1520 // Legal if the given handlerClass fails to be instantiated 1521 throw new ServletException("HTTP upgrade is not supported"); 1522 } 1523 1524 @Override 1525 @Nullable 1526 public Object getAttribute(@Nullable String name) { 1527 if (name == null) 1528 return null; 1529 1530 return getAttributes().get(name); 1531 } 1532 1533 @Override 1534 @NonNull 1535 public Enumeration<@NonNull String> getAttributeNames() { 1536 return Collections.enumeration(getAttributes().keySet()); 1537 } 1538 1539 @Override 1540 @Nullable 1541 public String getCharacterEncoding() { 1542 Charset explicit = getCharset().orElse(null); 1543 1544 if (explicit != null) 1545 return explicit.name(); 1546 1547 Charset context = getContextRequestCharset(); 1548 return context == null ? null : context.name(); 1549 } 1550 1551 @Override 1552 public void setCharacterEncoding(@Nullable String env) throws UnsupportedEncodingException { 1553 // Note that spec says: "This method must be called prior to reading request parameters or 1554 // reading input using getReader(). Otherwise, it has no effect." 1555 if (this.parametersAccessed || getRequestReadMethod() != RequestReadMethod.UNSPECIFIED) 1556 return; 1557 1558 if (env == null) { 1559 setCharset(null); 1560 } else { 1561 try { 1562 setCharset(Charset.forName(env)); 1563 } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 1564 throw new UnsupportedEncodingException(format("Not sure how to handle character encoding '%s'", env)); 1565 } 1566 } 1567 1568 this.queryParameters = null; 1569 this.formParameters = null; 1570 } 1571 1572 @Override 1573 public int getContentLength() { 1574 Long length = getContentLengthHeaderValue(); 1575 1576 if (length != null) { 1577 if (length > Integer.MAX_VALUE) 1578 return -1; 1579 1580 return length.intValue(); 1581 } 1582 1583 if (hasContentLengthHeader()) 1584 return -1; 1585 1586 byte[] body = getRequest().getBody().orElse(null); 1587 1588 if (body == null || body.length > Integer.MAX_VALUE) 1589 return -1; 1590 1591 return body.length; 1592 } 1593 1594 @Override 1595 public long getContentLengthLong() { 1596 Long length = getContentLengthHeaderValue(); 1597 1598 if (length != null) 1599 return length; 1600 1601 if (hasContentLengthHeader()) 1602 return -1; 1603 1604 byte[] body = getRequest().getBody().orElse(null); 1605 return body == null ? -1 : body.length; 1606 } 1607 1608 @Override 1609 @Nullable 1610 public String getContentType() { 1611 String headerValue = getHeader("Content-Type"); 1612 return headerValue != null ? headerValue : this.contentType; 1613 } 1614 1615 @Override 1616 @NonNull 1617 public ServletInputStream getInputStream() throws IOException { 1618 RequestReadMethod currentReadMethod = getRequestReadMethod(); 1619 1620 if (currentReadMethod == RequestReadMethod.UNSPECIFIED) { 1621 setRequestReadMethod(RequestReadMethod.INPUT_STREAM); 1622 byte[] body = this.bodyParametersAccessed ? new byte[]{} : getRequest().getBody().orElse(new byte[]{}); 1623 setServletInputStream(SokletServletInputStream.fromInputStream(new ByteArrayInputStream(body))); 1624 return getServletInputStream().get(); 1625 } else if (currentReadMethod == RequestReadMethod.INPUT_STREAM) { 1626 return getServletInputStream().get(); 1627 } else { 1628 throw new IllegalStateException("getReader() has already been called for this request"); 1629 } 1630 } 1631 1632 @Override 1633 @Nullable 1634 public String getParameter(@Nullable String name) { 1635 if (name == null) 1636 return null; 1637 1638 markParametersAccessed(); 1639 1640 Set<@NonNull String> queryValues = getQueryParameters().get(name); 1641 1642 if (queryValues != null && !queryValues.isEmpty()) 1643 return queryValues.iterator().next(); 1644 1645 Set<@NonNull String> formValues = getFormParameters().get(name); 1646 1647 if (formValues != null && !formValues.isEmpty()) 1648 return formValues.iterator().next(); 1649 1650 return null; 1651 } 1652 1653 @Override 1654 @NonNull 1655 public Enumeration<@NonNull String> getParameterNames() { 1656 markParametersAccessed(); 1657 1658 Set<@NonNull String> queryParameterNames = getQueryParameters().keySet(); 1659 Set<@NonNull String> formParameterNames = getFormParameters().keySet(); 1660 1661 Set<@NonNull String> parameterNames = new LinkedHashSet<>(queryParameterNames.size() + formParameterNames.size()); 1662 parameterNames.addAll(queryParameterNames); 1663 parameterNames.addAll(formParameterNames); 1664 1665 return Collections.enumeration(parameterNames); 1666 } 1667 1668 @Override 1669 public @NonNull String @Nullable [] getParameterValues(@Nullable String name) { 1670 if (name == null) 1671 return null; 1672 1673 markParametersAccessed(); 1674 1675 List<@NonNull String> parameterValues = new ArrayList<>(); 1676 1677 Set<@NonNull String> queryValues = getQueryParameters().get(name); 1678 1679 if (queryValues != null) 1680 parameterValues.addAll(queryValues); 1681 1682 Set<@NonNull String> formValues = getFormParameters().get(name); 1683 1684 if (formValues != null) 1685 parameterValues.addAll(formValues); 1686 1687 return parameterValues.isEmpty() ? null : parameterValues.toArray(new String[0]); 1688 } 1689 1690 @Override 1691 @NonNull 1692 public Map<@NonNull String, @NonNull String @NonNull []> getParameterMap() { 1693 markParametersAccessed(); 1694 1695 Map<@NonNull String, @NonNull Set<@NonNull String>> parameterMap = new LinkedHashMap<>(); 1696 1697 // Mutable copy of entries 1698 for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : getQueryParameters().entrySet()) 1699 parameterMap.put(entry.getKey(), new LinkedHashSet<>(entry.getValue())); 1700 1701 // Add form parameters to entries 1702 for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : getFormParameters().entrySet()) { 1703 Set<@NonNull String> existingEntries = parameterMap.get(entry.getKey()); 1704 1705 if (existingEntries != null) 1706 existingEntries.addAll(entry.getValue()); 1707 else 1708 parameterMap.put(entry.getKey(), new LinkedHashSet<>(entry.getValue())); 1709 } 1710 1711 Map<@NonNull String, @NonNull String @NonNull []> finalParameterMap = new LinkedHashMap<>(); 1712 1713 for (Entry<@NonNull String, @NonNull Set<@NonNull String>> entry : parameterMap.entrySet()) 1714 finalParameterMap.put(entry.getKey(), entry.getValue().toArray(new String[0])); 1715 1716 return Collections.unmodifiableMap(finalParameterMap); 1717 } 1718 1719 @Override 1720 @NonNull 1721 public String getProtocol() { 1722 return "HTTP/1.1"; 1723 } 1724 1725 @Override 1726 @NonNull 1727 public String getScheme() { 1728 URI effectiveOriginUri = getEffectiveOriginUri().orElse(null); 1729 1730 if (effectiveOriginUri != null && effectiveOriginUri.getScheme() != null) 1731 return effectiveOriginUri.getScheme().trim().toLowerCase(ROOT); 1732 1733 // Honor common reverse-proxy header only when trusted; fall back to http 1734 if (shouldTrustForwardedHeaders()) { 1735 String proto = getRequest().getHeader("X-Forwarded-Proto").orElse(null); 1736 1737 if (proto != null) { 1738 proto = proto.trim().toLowerCase(ROOT); 1739 if (proto.equals("https") || proto.equals("http")) 1740 return proto; 1741 } 1742 } 1743 1744 return "http"; 1745 } 1746 1747 @Override 1748 @NonNull 1749 public String getServerName() { 1750 URI effectiveOriginUri = getEffectiveOriginUri().orElse(null); 1751 1752 if (effectiveOriginUri != null) { 1753 String host = effectiveOriginUri.getHost(); 1754 1755 if (host == null) 1756 host = hostFromAuthority(effectiveOriginUri.getAuthority()); 1757 1758 if (host != null) { 1759 if (host.startsWith("[") && host.endsWith("]") && host.length() > 2) 1760 host = host.substring(1, host.length() - 1); 1761 1762 return host; 1763 } 1764 } 1765 1766 String hostHeader = getRequest().getHeader("Host").orElse(null); 1767 1768 if (hostHeader != null) { 1769 String host = hostFromAuthority(hostHeader); 1770 1771 if (host != null && !host.isBlank()) 1772 return host; 1773 } 1774 1775 return getLocalName(); 1776 } 1777 1778 @Override 1779 public int getServerPort() { 1780 URI effectiveOriginUri = getEffectiveOriginUri().orElse(null); 1781 1782 if (effectiveOriginUri != null) { 1783 int port = effectiveOriginUri.getPort(); 1784 if (port >= 0) 1785 return port; 1786 1787 Integer authorityPort = portFromAuthority(effectiveOriginUri.getAuthority()); 1788 1789 if (authorityPort != null) 1790 return authorityPort; 1791 1792 return defaultPortForScheme(effectiveOriginUri.getScheme()); 1793 } 1794 1795 String hostHeader = getRequest().getHeader("Host").orElse(null); 1796 1797 if (hostHeader != null) { 1798 Integer hostPort = portFromAuthority(hostHeader); 1799 1800 if (hostPort != null) 1801 return hostPort; 1802 } 1803 1804 Integer port = getPort().orElse(null); 1805 1806 if (port != null) 1807 return port; 1808 1809 int defaultPort = defaultPortForScheme(getScheme()); 1810 return defaultPort > 0 ? defaultPort : 0; 1811 } 1812 1813 @Override 1814 @NonNull 1815 public BufferedReader getReader() throws IOException { 1816 RequestReadMethod currentReadMethod = getRequestReadMethod(); 1817 1818 if (currentReadMethod == RequestReadMethod.UNSPECIFIED) { 1819 setRequestReadMethod(RequestReadMethod.READER); 1820 Charset charset = getEffectiveCharset(); 1821 byte[] body = this.bodyParametersAccessed ? new byte[]{} : getRequest().getBody().orElse(new byte[0]); 1822 InputStream inputStream = new ByteArrayInputStream(body); 1823 setBufferedReader(new BufferedReader(new InputStreamReader(inputStream, charset))); 1824 return getBufferedReader().get(); 1825 } else if (currentReadMethod == RequestReadMethod.READER) { 1826 return getBufferedReader().get(); 1827 } else { 1828 throw new IllegalStateException("getInputStream() has already been called for this request"); 1829 } 1830 } 1831 1832 @Override 1833 @Nullable 1834 public String getRemoteAddr() { 1835 if (shouldTrustForwardedHeaders()) { 1836 ForwardedClient forwardedFor = extractForwardedClientFromHeaders(); 1837 1838 if (forwardedFor != null) 1839 return forwardedFor.getHost(); 1840 1841 ForwardedClient xForwardedFor = extractXForwardedClientFromHeaders(); 1842 1843 if (xForwardedFor != null) 1844 return xForwardedFor.getHost(); 1845 } 1846 1847 InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null); 1848 1849 if (remoteAddress != null) { 1850 InetAddress address = remoteAddress.getAddress(); 1851 String host = address != null ? address.getHostAddress() : remoteAddress.getHostString(); 1852 1853 if (host != null && !host.isBlank()) 1854 return host; 1855 } 1856 1857 return null; 1858 } 1859 1860 @Override 1861 @Nullable 1862 public String getRemoteHost() { 1863 // "If the engine cannot or chooses not to resolve the hostname (to improve performance), 1864 // this method returns the dotted-string form of the IP address." 1865 return getRemoteAddr(); 1866 } 1867 1868 @Override 1869 public void setAttribute(@Nullable String name, 1870 @Nullable Object o) { 1871 if (name == null) 1872 return; 1873 1874 if (o == null) 1875 removeAttribute(name); 1876 else 1877 getAttributes().put(name, o); 1878 } 1879 1880 @Override 1881 public void removeAttribute(@Nullable String name) { 1882 if (name == null) 1883 return; 1884 1885 getAttributes().remove(name); 1886 } 1887 1888 @Override 1889 @NonNull 1890 public Locale getLocale() { 1891 List<@NonNull Locale> locales = getRequest().getLocales(); 1892 return locales.size() == 0 ? getDefault() : locales.get(0); 1893 } 1894 1895 @Override 1896 @NonNull 1897 public Enumeration<@NonNull Locale> getLocales() { 1898 List<@NonNull Locale> locales = getRequest().getLocales(); 1899 return Collections.enumeration(locales.size() == 0 ? List.of(getDefault()) : locales); 1900 } 1901 1902 @Override 1903 public boolean isSecure() { 1904 return getScheme().equals("https"); 1905 } 1906 1907 @Override 1908 @Nullable 1909 public RequestDispatcher getRequestDispatcher(@Nullable String path) { 1910 // "This method returns null if the servlet container cannot return a RequestDispatcher." 1911 return null; 1912 } 1913 1914 @Override 1915 @Deprecated 1916 @Nullable 1917 public String getRealPath(String path) { 1918 // "As of Version 2.1 of the Java Servlet API, use ServletContext.getRealPath(java.lang.String) instead." 1919 return getServletContext().getRealPath(path); 1920 } 1921 1922 @Override 1923 public int getRemotePort() { 1924 if (shouldTrustForwardedHeaders()) { 1925 ForwardedClient forwardedFor = extractForwardedClientFromHeaders(); 1926 1927 if (forwardedFor != null) { 1928 Integer port = forwardedFor.getPort(); 1929 return port == null ? 0 : port; 1930 } 1931 1932 ForwardedClient xForwardedFor = extractXForwardedClientFromHeaders(); 1933 1934 if (xForwardedFor != null) { 1935 Integer port = xForwardedFor.getPort(); 1936 return port == null ? 0 : port; 1937 } 1938 } 1939 1940 InetSocketAddress remoteAddress = getRequest().getRemoteAddress().orElse(null); 1941 return remoteAddress == null ? 0 : remoteAddress.getPort(); 1942 } 1943 1944 @Override 1945 @NonNull 1946 public String getLocalName() { 1947 String host = getHost().orElse(null); 1948 1949 if (host != null && !host.isBlank()) 1950 return stripIpv6Brackets(host); 1951 1952 return "localhost"; 1953 } 1954 1955 @Override 1956 @NonNull 1957 public String getLocalAddr() { 1958 String host = getHost().orElse(null); 1959 1960 if (host != null) { 1961 String normalized = stripIpv6Brackets(host).trim(); 1962 1963 if (!normalized.isEmpty() && (isIpv4Literal(normalized) || isIpv6Literal(normalized))) 1964 return normalized; 1965 } 1966 1967 return "127.0.0.1"; 1968 } 1969 1970 @Override 1971 public int getLocalPort() { 1972 Integer port = getPort().orElse(null); 1973 return port == null ? 0 : port; 1974 } 1975 1976 @Override 1977 @NonNull 1978 public ServletContext getServletContext() { 1979 return this.servletContext; 1980 } 1981 1982 @Override 1983 @NonNull 1984 public AsyncContext startAsync() throws IllegalStateException { 1985 throw new IllegalStateException("Soklet does not support async servlet operations"); 1986 } 1987 1988 @Override 1989 @NonNull 1990 public AsyncContext startAsync(@NonNull ServletRequest servletRequest, 1991 @NonNull ServletResponse servletResponse) throws IllegalStateException { 1992 requireNonNull(servletRequest); 1993 requireNonNull(servletResponse); 1994 1995 throw new IllegalStateException("Soklet does not support async servlet operations"); 1996 } 1997 1998 @Override 1999 public boolean isAsyncStarted() { 2000 return false; 2001 } 2002 2003 @Override 2004 public boolean isAsyncSupported() { 2005 return false; 2006 } 2007 2008 @Override 2009 @NonNull 2010 public AsyncContext getAsyncContext() { 2011 throw new IllegalStateException("Soklet does not support async servlet operations"); 2012 } 2013 2014 @Override 2015 @NonNull 2016 public DispatcherType getDispatcherType() { 2017 // Currently Soklet does not support RequestDispatcher, so this is safe to hardcode 2018 return DispatcherType.REQUEST; 2019 } 2020}