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