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}