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