001/* 002 * Copyright 2024-2025 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.Request; 020import com.soklet.Utilities; 021 022import javax.annotation.Nonnull; 023import javax.annotation.Nullable; 024import javax.annotation.concurrent.NotThreadSafe; 025import javax.servlet.AsyncContext; 026import javax.servlet.DispatcherType; 027import javax.servlet.RequestDispatcher; 028import javax.servlet.ServletContext; 029import javax.servlet.ServletException; 030import javax.servlet.ServletInputStream; 031import javax.servlet.ServletRequest; 032import javax.servlet.ServletResponse; 033import javax.servlet.http.Cookie; 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036import javax.servlet.http.HttpSession; 037import javax.servlet.http.HttpUpgradeHandler; 038import javax.servlet.http.Part; 039import java.io.BufferedReader; 040import java.io.ByteArrayInputStream; 041import java.io.IOException; 042import java.io.InputStream; 043import java.io.InputStreamReader; 044import java.io.UnsupportedEncodingException; 045import java.net.InetAddress; 046import java.net.URI; 047import java.nio.charset.Charset; 048import java.nio.charset.IllegalCharsetNameException; 049import java.nio.charset.StandardCharsets; 050import java.nio.charset.UnsupportedCharsetException; 051import java.security.Principal; 052import java.time.Instant; 053import java.time.ZoneOffset; 054import java.time.format.DateTimeFormatter; 055import java.time.format.DateTimeFormatterBuilder; 056import java.time.format.SignStyle; 057import java.time.temporal.ChronoField; 058import java.util.ArrayList; 059import java.util.Collection; 060import java.util.Collections; 061import java.util.Enumeration; 062import java.util.HashMap; 063import java.util.HashSet; 064import java.util.List; 065import java.util.Locale; 066import java.util.Map; 067import java.util.Map.Entry; 068import java.util.Optional; 069import java.util.Set; 070import java.util.TreeMap; 071import java.util.UUID; 072 073import static java.lang.String.format; 074import static java.util.Locale.ROOT; 075import static java.util.Locale.US; 076import static java.util.Locale.getDefault; 077import static java.util.Objects.requireNonNull; 078 079/** 080 * Soklet integration implementation of {@link HttpServletRequest}. 081 * 082 * @author <a href="https://www.revetkn.com">Mark Allen</a> 083 */ 084@NotThreadSafe 085public final class SokletHttpServletRequest implements HttpServletRequest { 086 @Nonnull 087 private static final Charset DEFAULT_CHARSET; 088 @Nonnull 089 private static final DateTimeFormatter RFC_1123_PARSER; 090 @Nonnull 091 private static final DateTimeFormatter RFC_1036_PARSER; 092 @Nonnull 093 private static final DateTimeFormatter ASCTIME_PARSER; 094 095 static { 096 DEFAULT_CHARSET = StandardCharsets.ISO_8859_1; // Per Servlet spec 097 RFC_1123_PARSER = DateTimeFormatter.RFC_1123_DATE_TIME; 098 // RFC 1036: spaces between day/month/year + 2-digit year reduced to 19xx baseline. 099 RFC_1036_PARSER = new DateTimeFormatterBuilder() 100 .parseCaseInsensitive() 101 .appendPattern("EEE, dd MMM ") 102 .appendValueReduced(ChronoField.YEAR, 2, 2, 1900) // 94 -> 1994 103 .appendPattern(" HH:mm:ss zzz") 104 .toFormatter(US) 105 .withZone(ZoneOffset.UTC); 106 107 // asctime: "EEE MMM d HH:mm:ss yyyy" — allow 1 or 2 spaces before day, no zone in text → default GMT. 108 ASCTIME_PARSER = new DateTimeFormatterBuilder() 109 .parseCaseInsensitive() 110 .appendPattern("EEE MMM") 111 .appendLiteral(' ') 112 .optionalStart().appendLiteral(' ').optionalEnd() // tolerate double space before single-digit day 113 .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE) 114 .appendPattern(" HH:mm:ss yyyy") 115 .toFormatter(US) 116 .withZone(ZoneOffset.UTC); 117 } 118 119 @Nonnull 120 private final Request request; 121 @Nullable 122 private final String host; 123 @Nullable 124 private final Integer port; 125 @Nonnull 126 private final ServletContext servletContext; 127 @Nullable 128 private HttpSession httpSession; 129 @Nonnull 130 private final Map<String, Object> attributes; 131 @Nonnull 132 private final List<Cookie> cookies; 133 @Nullable 134 private Charset charset; 135 @Nullable 136 private String contentType; 137 138 @Nonnull 139 public static Builder withRequest(@Nonnull Request request) { 140 return new Builder(request); 141 } 142 143 private SokletHttpServletRequest(@Nonnull Builder builder) { 144 requireNonNull(builder); 145 requireNonNull(builder.request); 146 147 this.request = builder.request; 148 this.attributes = new HashMap<>(); 149 this.cookies = parseCookies(request); 150 this.charset = parseCharacterEncoding(request).orElse(null); 151 this.contentType = parseContentType(request).orElse(null); 152 this.host = builder.host; 153 this.port = builder.port; 154 this.servletContext = builder.servletContext == null ? SokletServletContext.withDefaults() : builder.servletContext; 155 this.httpSession = builder.httpSession; 156 } 157 158 @Nonnull 159 protected Request getRequest() { 160 return this.request; 161 } 162 163 @Nonnull 164 protected Map<String, Object> getAttributes() { 165 return this.attributes; 166 } 167 168 @Nonnull 169 protected List<Cookie> parseCookies(@Nonnull Request request) { 170 requireNonNull(request); 171 172 Map<String, Set<String>> cookies = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 173 cookies.putAll(request.getCookies()); 174 175 List<Cookie> convertedCookies = new ArrayList<>(cookies.size()); 176 177 for (Entry<String, Set<String>> entry : cookies.entrySet()) { 178 String name = entry.getKey(); 179 Set<String> values = entry.getValue(); 180 181 // Should never occur... 182 if (name == null) 183 continue; 184 185 for (String value : values) 186 convertedCookies.add(new Cookie(name, value)); 187 } 188 189 return convertedCookies; 190 } 191 192 @Nonnull 193 protected Optional<Charset> parseCharacterEncoding(@Nonnull Request request) { 194 requireNonNull(request); 195 return Utilities.extractCharsetFromHeaders(request.getHeaders()); 196 } 197 198 @Nonnull 199 protected Optional<String> parseContentType(@Nonnull Request request) { 200 requireNonNull(request); 201 return Utilities.extractContentTypeFromHeaders(request.getHeaders()); 202 } 203 204 @Nonnull 205 protected Optional<HttpSession> getHttpSession() { 206 return Optional.ofNullable(this.httpSession); 207 } 208 209 protected void setHttpSession(@Nullable HttpSession httpSession) { 210 this.httpSession = httpSession; 211 } 212 213 @Nonnull 214 protected Optional<Charset> getCharset() { 215 return Optional.ofNullable(this.charset); 216 } 217 218 protected void setCharset(@Nullable Charset charset) { 219 this.charset = charset; 220 } 221 222 @Nonnull 223 protected Optional<String> getHost() { 224 return Optional.ofNullable(this.host); 225 } 226 227 @Nonnull 228 protected Optional<Integer> getPort() { 229 return Optional.ofNullable(this.port); 230 } 231 232 /** 233 * Builder used to construct instances of {@link SokletHttpServletRequest}. 234 * <p> 235 * This class is intended for use by a single thread. 236 * 237 * @author <a href="https://www.revetkn.com">Mark Allen</a> 238 */ 239 @NotThreadSafe 240 public static class Builder { 241 @Nonnull 242 private Request request; 243 @Nullable 244 private Integer port; 245 @Nullable 246 private String host; 247 @Nullable 248 private ServletContext servletContext; 249 @Nullable 250 private HttpSession httpSession; 251 252 @Nonnull 253 private Builder(@Nonnull Request request) { 254 requireNonNull(request); 255 this.request = request; 256 } 257 258 @Nonnull 259 public Builder request(@Nonnull Request request) { 260 requireNonNull(request); 261 this.request = request; 262 return this; 263 } 264 265 @Nonnull 266 public Builder host(@Nullable String host) { 267 this.host = host; 268 return this; 269 } 270 271 @Nonnull 272 public Builder port(@Nullable Integer port) { 273 this.port = port; 274 return this; 275 } 276 277 @Nonnull 278 public Builder servletContext(@Nullable ServletContext servletContext) { 279 this.servletContext = servletContext; 280 return this; 281 } 282 283 @Nonnull 284 public Builder httpSession(@Nullable HttpSession httpSession) { 285 this.httpSession = httpSession; 286 return this; 287 } 288 289 @Nonnull 290 public SokletHttpServletRequest build() { 291 return new SokletHttpServletRequest(this); 292 } 293 } 294 295 // Implementation of HttpServletRequest methods below: 296 297 // Helpful reference at https://stackoverflow.com/a/21046620 by Victor Stafusa - BozoNaCadeia 298 // 299 // Method URL-Decoded Result 300 // ---------------------------------------------------- 301 // getContextPath() no /app 302 // getLocalAddr() 127.0.0.1 303 // getLocalName() 30thh.loc 304 // getLocalPort() 8480 305 // getMethod() GET 306 // getPathInfo() yes /a?+b 307 // getProtocol() HTTP/1.1 308 // getQueryString() no p+1=c+d&p+2=e+f 309 // getRequestedSessionId() no S%3F+ID 310 // getRequestURI() no /app/test%3F/a%3F+b;jsessionid=S+ID 311 // getRequestURL() no http://30thh.loc:8480/app/test%3F/a%3F+b;jsessionid=S+ID 312 // getScheme() http 313 // getServerName() 30thh.loc 314 // getServerPort() 8480 315 // getServletPath() yes /test? 316 // getParameterNames() yes [p 2, p 1] 317 // getParameter("p 1") yes c d 318 319 @Override 320 @Nullable 321 public String getAuthType() { 322 // This is legal according to spec 323 return null; 324 } 325 326 @Override 327 @Nonnull 328 public Cookie[] getCookies() { 329 return this.cookies.toArray(new Cookie[0]); 330 } 331 332 @Override 333 public long getDateHeader(@Nullable String name) { 334 if (name == null) 335 return -1; 336 337 String value = getHeader(name); 338 339 if (value == null) 340 return -1; 341 342 // Try HTTP-date formats (RFC 1123 → RFC 1036 → asctime) 343 for (DateTimeFormatter fmt : List.of(RFC_1123_PARSER, RFC_1036_PARSER, ASCTIME_PARSER)) { 344 try { 345 return Instant.from(fmt.parse(value)).toEpochMilli(); 346 } catch (Exception ignored) { 347 // try next 348 } 349 } 350 351 // Fallback: epoch millis 352 try { 353 return Long.parseLong(value); 354 } catch (NumberFormatException e) { 355 throw new IllegalArgumentException( 356 String.format("Header with name '%s' and value '%s' cannot be converted to a date", name, value), 357 e 358 ); 359 } 360 } 361 362 @Override 363 @Nullable 364 public String getHeader(@Nullable String name) { 365 if (name == null) 366 return null; 367 368 return getRequest().getHeader(name).orElse(null); 369 } 370 371 @Override 372 @Nonnull 373 public Enumeration<String> getHeaders(@Nullable String name) { 374 if (name == null) 375 return Collections.emptyEnumeration(); 376 377 Set<String> values = request.getHeaders().get(name); 378 return values == null ? Collections.emptyEnumeration() : Collections.enumeration(values); 379 } 380 381 @Override 382 @Nonnull 383 public Enumeration<String> getHeaderNames() { 384 return Collections.enumeration(getRequest().getHeaders().keySet()); 385 } 386 387 @Override 388 public int getIntHeader(@Nullable String name) { 389 if (name == null) 390 return -1; 391 392 String value = getHeader(name); 393 394 if (value == null) 395 return -1; 396 397 // Throws NumberFormatException if parsing fails, per spec 398 return Integer.valueOf(value, 10); 399 } 400 401 @Override 402 @Nonnull 403 public String getMethod() { 404 return getRequest().getHttpMethod().name(); 405 } 406 407 @Override 408 @Nullable 409 public String getPathInfo() { 410 return getRequest().getPath(); 411 } 412 413 @Override 414 @Nullable 415 public String getPathTranslated() { 416 return getRequest().getPath(); 417 } 418 419 @Override 420 @Nonnull 421 public String getContextPath() { 422 return ""; 423 } 424 425 @Override 426 @Nullable 427 public String getQueryString() { 428 try { 429 URI uri = new URI(request.getUri()); 430 return uri.getQuery(); 431 } catch (Exception ignored) { 432 return null; 433 } 434 } 435 436 @Override 437 @Nullable 438 public String getRemoteUser() { 439 // This is legal according to spec 440 return null; 441 } 442 443 @Override 444 public boolean isUserInRole(@Nullable String role) { 445 // This is legal according to spec 446 return false; 447 } 448 449 @Override 450 @Nullable 451 public Principal getUserPrincipal() { 452 // This is legal according to spec 453 return null; 454 } 455 456 @Override 457 @Nullable 458 public String getRequestedSessionId() { 459 // This is legal according to spec 460 return null; 461 } 462 463 @Override 464 @Nonnull 465 public String getRequestURI() { 466 return getRequest().getPath(); 467 } 468 469 @Override 470 @Nonnull 471 public StringBuffer getRequestURL() { 472 // Try forwarded/synthesized absolute prefix first 473 String clientUrlPrefix = Utilities.extractClientUrlPrefixFromHeaders(getRequest().getHeaders()).orElse(null); 474 475 if (clientUrlPrefix != null) 476 return new StringBuffer(format("%s%s", clientUrlPrefix, getRequest().getPath())); 477 478 // Fall back to builder-provided host/port when available 479 String scheme = getScheme(); // Soklet returns "http" by design 480 String host = getServerName(); 481 int port = getServerPort(); // may throw if not initialized by builder 482 boolean defaultPort = ("https".equalsIgnoreCase(scheme) && port == 443) || ("http".equalsIgnoreCase(scheme) && port == 80); 483 String authority = defaultPort ? host : format("%s:%d", host, port); 484 return new StringBuffer(format("%s://%s%s", scheme, authority, getRequest().getPath())); 485 } 486 487 @Override 488 @Nonnull 489 public String getServletPath() { 490 // This is legal according to spec 491 return ""; 492 } 493 494 @Override 495 @Nullable 496 public HttpSession getSession(boolean create) { 497 HttpSession currentHttpSession = getHttpSession().orElse(null); 498 499 if (create && currentHttpSession == null) { 500 currentHttpSession = SokletHttpSession.withServletContext(getServletContext()); 501 setHttpSession(currentHttpSession); 502 } 503 504 return currentHttpSession; 505 } 506 507 @Override 508 @Nonnull 509 public HttpSession getSession() { 510 HttpSession currentHttpSession = getHttpSession().orElse(null); 511 512 if (currentHttpSession == null) { 513 currentHttpSession = SokletHttpSession.withServletContext(getServletContext()); 514 setHttpSession(currentHttpSession); 515 } 516 517 return currentHttpSession; 518 } 519 520 @Override 521 @Nonnull 522 public String changeSessionId() { 523 HttpSession currentHttpSession = getHttpSession().orElse(null); 524 525 if (currentHttpSession == null) 526 throw new IllegalStateException("No session is present"); 527 528 if (!(currentHttpSession instanceof SokletHttpSession)) 529 throw new IllegalStateException(format("Cannot change session IDs. Session must be of type %s; instead it is of type %s", 530 SokletHttpSession.class.getSimpleName(), currentHttpSession.getClass().getSimpleName())); 531 532 UUID newSessionId = UUID.randomUUID(); 533 ((SokletHttpSession) currentHttpSession).setSessionId(newSessionId); 534 return String.valueOf(newSessionId); 535 } 536 537 @Override 538 public boolean isRequestedSessionIdValid() { 539 // This is legal according to spec 540 return false; 541 } 542 543 @Override 544 public boolean isRequestedSessionIdFromCookie() { 545 // This is legal according to spec 546 return false; 547 } 548 549 @Override 550 public boolean isRequestedSessionIdFromURL() { 551 // This is legal according to spec 552 return false; 553 } 554 555 @Override 556 @Deprecated 557 public boolean isRequestedSessionIdFromUrl() { 558 // This is legal according to spec 559 return false; 560 } 561 562 @Override 563 public boolean authenticate(@Nonnull HttpServletResponse httpServletResponse) throws IOException, ServletException { 564 requireNonNull(httpServletResponse); 565 // TODO: perhaps revisit this in the future 566 throw new ServletException("Authentication is not supported"); 567 } 568 569 @Override 570 public void login(@Nullable String username, 571 @Nullable String password) throws ServletException { 572 // This is legal according to spec 573 throw new ServletException("Authentication login is not supported"); 574 } 575 576 @Override 577 public void logout() throws ServletException { 578 // This is legal according to spec 579 throw new ServletException("Authentication logout is not supported"); 580 } 581 582 @Override 583 @Nonnull 584 public Collection<Part> getParts() throws IOException, ServletException { 585 // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize, 586 // or there is no @MultipartConfig or multipart-config in deployment descriptors 587 throw new IllegalStateException("Servlet multipart configuration is not supported"); 588 } 589 590 @Override 591 @Nullable 592 public Part getPart(@Nullable String name) throws IOException, ServletException { 593 // Legal if the request body is larger than maxRequestSize, or any Part in the request is larger than maxFileSize, 594 // or there is no @MultipartConfig or multipart-config in deployment descriptors 595 throw new IllegalStateException("Servlet multipart configuration is not supported"); 596 } 597 598 @Override 599 @Nonnull 600 public <T extends HttpUpgradeHandler> T upgrade(@Nullable Class<T> handlerClass) throws IOException, ServletException { 601 // Legal if the given handlerClass fails to be instantiated 602 throw new ServletException("HTTP upgrade is not supported"); 603 } 604 605 @Override 606 @Nullable 607 public Object getAttribute(@Nullable String name) { 608 if (name == null) 609 return null; 610 611 return getAttributes().get(name); 612 } 613 614 @Override 615 @Nonnull 616 public Enumeration<String> getAttributeNames() { 617 return Collections.enumeration(getAttributes().keySet()); 618 } 619 620 @Override 621 @Nonnull 622 public String getCharacterEncoding() { 623 Charset charset = getCharset().orElse(null); 624 return charset == null ? null : charset.name(); 625 } 626 627 @Override 628 public void setCharacterEncoding(@Nullable String env) throws UnsupportedEncodingException { 629 // Note that spec says: "This method must be called prior to reading request parameters or 630 // reading input using getReader(). Otherwise, it has no effect." 631 // ...but we don't need to care about this because Soklet requests are byte arrays of finite size, not streams 632 if (env == null) { 633 setCharset(null); 634 } else { 635 try { 636 setCharset(Charset.forName(env)); 637 } catch (IllegalCharsetNameException | UnsupportedCharsetException e) { 638 throw new UnsupportedEncodingException(format("Not sure how to handle character encoding '%s'", env)); 639 } 640 } 641 } 642 643 @Override 644 public int getContentLength() { 645 byte[] body = request.getBody().orElse(null); 646 return body == null ? 0 : body.length; 647 } 648 649 @Override 650 public long getContentLengthLong() { 651 byte[] body = request.getBody().orElse(null); 652 return body == null ? 0 : body.length; 653 } 654 655 @Override 656 @Nullable 657 public String getContentType() { 658 return this.contentType; 659 } 660 661 @Override 662 @Nonnull 663 public ServletInputStream getInputStream() throws IOException { 664 byte[] body = getRequest().getBody().orElse(new byte[]{}); 665 return SokletServletInputStream.withInputStream(new ByteArrayInputStream(body)); 666 } 667 668 @Override 669 @Nullable 670 public String getParameter(@Nullable String name) { 671 String value = null; 672 673 // First, check query parameters. 674 if (getRequest().getQueryParameters().keySet().contains(name)) { 675 // If there is a query parameter with the given name, return it 676 value = getRequest().getQueryParameter(name).orElse(null); 677 } else if (getRequest().getFormParameters().keySet().contains(name)) { 678 // Otherwise, check form parameters in request body 679 value = getRequest().getFormParameter(name).orElse(null); 680 } 681 682 return value; 683 } 684 685 @Override 686 @Nonnull 687 public Enumeration<String> getParameterNames() { 688 Set<String> queryParameterNames = getRequest().getQueryParameters().keySet(); 689 Set<String> formParameterNames = getRequest().getFormParameters().keySet(); 690 691 Set<String> parameterNames = new HashSet<>(queryParameterNames.size() + formParameterNames.size()); 692 parameterNames.addAll(queryParameterNames); 693 parameterNames.addAll(formParameterNames); 694 695 return Collections.enumeration(parameterNames); 696 } 697 698 @Override 699 @Nullable 700 public String[] getParameterValues(@Nullable String name) { 701 if (name == null) 702 return null; 703 704 List<String> parameterValues = new ArrayList<>(); 705 706 Set<String> queryValues = getRequest().getQueryParameters().get(name); 707 708 if (queryValues != null) 709 parameterValues.addAll(queryValues); 710 711 Set<String> formValues = getRequest().getFormParameters().get(name); 712 713 if (formValues != null) 714 parameterValues.addAll(formValues); 715 716 return parameterValues.isEmpty() ? null : parameterValues.toArray(new String[0]); 717 } 718 719 @Override 720 @Nonnull 721 public Map<String, String[]> getParameterMap() { 722 Map<String, Set<String>> parameterMap = new HashMap<>(); 723 724 // Mutable copy of entries 725 for (Entry<String, Set<String>> entry : getRequest().getQueryParameters().entrySet()) 726 parameterMap.put(entry.getKey(), new HashSet<>(entry.getValue())); 727 728 // Add form parameters to entries 729 for (Entry<String, Set<String>> entry : getRequest().getFormParameters().entrySet()) { 730 Set<String> existingEntries = parameterMap.get(entry.getKey()); 731 732 if (existingEntries != null) 733 existingEntries.addAll(entry.getValue()); 734 else 735 parameterMap.put(entry.getKey(), entry.getValue()); 736 } 737 738 Map<String, String[]> finalParameterMap = new HashMap<>(); 739 740 for (Entry<String, Set<String>> entry : parameterMap.entrySet()) 741 finalParameterMap.put(entry.getKey(), entry.getValue().toArray(new String[0])); 742 743 return Collections.unmodifiableMap(finalParameterMap); 744 } 745 746 @Override 747 @Nonnull 748 public String getProtocol() { 749 return "HTTP/1.1"; 750 } 751 752 @Override 753 @Nonnull 754 public String getScheme() { 755 // Honor common reverse-proxy header; fall back to http 756 String proto = getRequest().getHeader("X-Forwarded-Proto").orElse(null); 757 758 if (proto != null) { 759 proto = proto.trim().toLowerCase(ROOT); 760 if (proto.equals("https") || proto.equals("http")) 761 return proto; 762 } 763 764 return "http"; 765 } 766 767 @Override 768 @Nonnull 769 public String getServerName() { 770 // Path only (no query parameters) preceded by remote protocol, host, and port (if available) 771 // e.g. https://www.soklet.com/test/abc 772 String clientUrlPrefix = Utilities.extractClientUrlPrefixFromHeaders(getRequest().getHeaders()).orElse(null); 773 774 if (clientUrlPrefix == null) 775 return getLocalName(); 776 777 clientUrlPrefix = clientUrlPrefix.toLowerCase(ROOT); 778 779 // Remove protocol prefix 780 if (clientUrlPrefix.startsWith("https://")) 781 clientUrlPrefix = clientUrlPrefix.replace("https://", ""); 782 else if (clientUrlPrefix.startsWith("http://")) 783 clientUrlPrefix = clientUrlPrefix.replace("http://", ""); 784 785 // Remove "/" and anything after it 786 int indexOfFirstSlash = clientUrlPrefix.indexOf("/"); 787 788 if (indexOfFirstSlash != -1) 789 clientUrlPrefix = clientUrlPrefix.substring(0, indexOfFirstSlash); 790 791 // Remove ":" and anything after it (port) 792 int indexOfColon = clientUrlPrefix.indexOf(":"); 793 794 if (indexOfColon != -1) 795 clientUrlPrefix = clientUrlPrefix.substring(0, indexOfColon); 796 797 return clientUrlPrefix; 798 } 799 800 @Override 801 public int getServerPort() { 802 // Path only (no query parameters) preceded by remote protocol, host, and port (if available) 803 // e.g. https://www.soklet.com/test/abc 804 String clientUrlPrefix = Utilities.extractClientUrlPrefixFromHeaders(getRequest().getHeaders()).orElse(null); 805 806 if (clientUrlPrefix == null) 807 return getLocalPort(); 808 809 clientUrlPrefix = clientUrlPrefix.toLowerCase(ROOT); 810 811 boolean https = false; 812 813 // Remove protocol prefix 814 if (clientUrlPrefix.startsWith("https://")) { 815 clientUrlPrefix = clientUrlPrefix.replace("https://", ""); 816 https = true; 817 } else if (clientUrlPrefix.startsWith("http://")) { 818 clientUrlPrefix = clientUrlPrefix.replace("http://", ""); 819 } 820 821 // Remove "/" and anything after it 822 int indexOfFirstSlash = clientUrlPrefix.indexOf("/"); 823 824 if (indexOfFirstSlash != -1) 825 clientUrlPrefix = clientUrlPrefix.substring(0, indexOfFirstSlash); 826 827 String[] hostAndPortComponents = clientUrlPrefix.split(":"); 828 829 // No explicit port? Look at protocol for guidance 830 if (hostAndPortComponents.length == 1) 831 return https ? 443 : 80; 832 833 try { 834 return Integer.parseInt(hostAndPortComponents[1], 10); 835 } catch (Exception ignored) { 836 return getLocalPort(); 837 } 838 } 839 840 @Override 841 @Nonnull 842 public BufferedReader getReader() throws IOException { 843 Charset charset = getCharset().orElse(DEFAULT_CHARSET); 844 InputStream inputStream = new ByteArrayInputStream(getRequest().getBody().orElse(new byte[0])); 845 return new BufferedReader(new InputStreamReader(inputStream, charset)); 846 } 847 848 @Override 849 @Nullable 850 public String getRemoteAddr() { 851 String xForwardedForHeader = getRequest().getHeader("X-Forwarded-For").orElse(null); 852 853 if (xForwardedForHeader == null) 854 return null; 855 856 // Example value: 203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,198.51.100.178 857 String[] components = xForwardedForHeader.split(","); 858 859 if (components.length == 0 || components[0] == null) 860 return null; 861 862 String value = components[0].trim(); 863 return value.length() > 0 ? value : "127.0.0.1"; 864 } 865 866 @Override 867 @Nullable 868 public String getRemoteHost() { 869 // This is X-Forwarded-For and is generally what we want (if present) 870 String remoteAddr = getRemoteAddr(); 871 872 if (remoteAddr != null) 873 return remoteAddr; 874 875 // Path only (no query parameters) preceded by remote protocol, host, and port (if available) 876 // e.g. https://www.soklet.com/test/abc 877 String clientUrlPrefix = Utilities.extractClientUrlPrefixFromHeaders(getRequest().getHeaders()).orElse(null); 878 879 if (clientUrlPrefix != null) { 880 clientUrlPrefix = clientUrlPrefix.toLowerCase(ROOT); 881 882 // Remove protocol prefix 883 if (clientUrlPrefix.startsWith("https://")) 884 clientUrlPrefix = clientUrlPrefix.replace("https://", ""); 885 else if (clientUrlPrefix.startsWith("http://")) 886 clientUrlPrefix = clientUrlPrefix.replace("http://", ""); 887 888 // Remove "/" and anything after it 889 int indexOfFirstSlash = clientUrlPrefix.indexOf("/"); 890 891 if (indexOfFirstSlash != -1) 892 clientUrlPrefix = clientUrlPrefix.substring(0, indexOfFirstSlash); 893 894 String[] hostAndPortComponents = clientUrlPrefix.split(":"); 895 896 String host = null; 897 898 if (hostAndPortComponents != null && hostAndPortComponents.length > 0 && hostAndPortComponents[0] != null) 899 host = hostAndPortComponents[0].trim(); 900 901 if (host != null && host.length() > 0) 902 return host; 903 } 904 905 // "If the engine cannot or chooses not to resolve the hostname (to improve performance), 906 // this method returns the dotted-string form of the IP address." 907 return getRemoteAddr(); 908 } 909 910 @Override 911 public void setAttribute(@Nullable String name, 912 @Nullable Object o) { 913 if (name == null) 914 return; 915 916 if (o == null) 917 removeAttribute(name); 918 else 919 getAttributes().put(name, o); 920 } 921 922 @Override 923 public void removeAttribute(@Nullable String name) { 924 if (name == null) 925 return; 926 927 getAttributes().remove(name); 928 } 929 930 @Override 931 @Nonnull 932 public Locale getLocale() { 933 List<Locale> locales = getRequest().getLocales(); 934 return locales.size() == 0 ? getDefault() : locales.get(0); 935 } 936 937 @Override 938 @Nonnull 939 public Enumeration<Locale> getLocales() { 940 List<Locale> locales = getRequest().getLocales(); 941 return Collections.enumeration(locales.size() == 0 ? List.of(getDefault()) : locales); 942 } 943 944 @Override 945 public boolean isSecure() { 946 return getScheme().equals("https"); 947 } 948 949 @Override 950 @Nullable 951 public RequestDispatcher getRequestDispatcher(@Nullable String path) { 952 // "This method returns null if the servlet container cannot return a RequestDispatcher." 953 return null; 954 } 955 956 @Override 957 @Deprecated 958 @Nullable 959 public String getRealPath(String path) { 960 // "As of Version 2.1 of the Java Servlet API, use ServletContext.getRealPath(java.lang.String) instead." 961 return getServletContext().getRealPath(path); 962 } 963 964 @Override 965 public int getRemotePort() { 966 // Not reliably knowable without a socket; return 0 to indicate "unknown" 967 return 0; 968 } 969 970 @Override 971 @Nonnull 972 public String getLocalName() { 973 if (getHost().isPresent()) 974 return getHost().get(); 975 976 try { 977 String hostName = InetAddress.getLocalHost().getHostName(); 978 979 if (hostName != null) { 980 hostName = hostName.trim(); 981 982 if (hostName.length() > 0) 983 return hostName; 984 } 985 } catch (Exception e) { 986 // Ignored 987 } 988 989 return "localhost"; 990 } 991 992 @Override 993 @Nonnull 994 public String getLocalAddr() { 995 try { 996 String hostAddress = InetAddress.getLocalHost().getHostAddress(); 997 998 if (hostAddress != null) { 999 hostAddress = hostAddress.trim(); 1000 1001 if (hostAddress.length() > 0) 1002 return hostAddress; 1003 } 1004 } catch (Exception e) { 1005 // Ignored 1006 } 1007 1008 return "127.0.0.1"; 1009 } 1010 1011 @Override 1012 public int getLocalPort() { 1013 return getPort().orElseThrow(() -> new IllegalStateException(format("%s must be initialized with a port in order to call this method", 1014 getClass().getSimpleName()))); 1015 } 1016 1017 @Override 1018 @Nonnull 1019 public ServletContext getServletContext() { 1020 return this.servletContext; 1021 } 1022 1023 @Override 1024 @Nonnull 1025 public AsyncContext startAsync() throws IllegalStateException { 1026 throw new IllegalStateException("Soklet does not support async servlet operations"); 1027 } 1028 1029 @Override 1030 @Nonnull 1031 public AsyncContext startAsync(@Nonnull ServletRequest servletRequest, 1032 @Nonnull ServletResponse servletResponse) throws IllegalStateException { 1033 requireNonNull(servletResponse); 1034 requireNonNull(servletResponse); 1035 1036 throw new IllegalStateException("Soklet does not support async servlet operations"); 1037 } 1038 1039 @Override 1040 public boolean isAsyncStarted() { 1041 return false; 1042 } 1043 1044 @Override 1045 public boolean isAsyncSupported() { 1046 return false; 1047 } 1048 1049 @Override 1050 @Nonnull 1051 public AsyncContext getAsyncContext() { 1052 throw new IllegalStateException("Soklet does not support async servlet operations"); 1053 } 1054 1055 @Override 1056 @Nonnull 1057 public DispatcherType getDispatcherType() { 1058 // Currently Soklet does not support RequestDispatcher, so this is safe to hardcode 1059 return DispatcherType.REQUEST; 1060 } 1061}